Skip to content

Zoisite Luwam Ghebremicael #121

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

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d3b6a25
added nullability
CodeWithLuwam May 9, 2023
3b04e8e
testing
CodeWithLuwam May 9, 2023
f00cd2d
Merge pull request #1 from luwammikael/wave-1
CodeWithLuwam May 9, 2023
ddb0972
completes get_one/get_all POST/GET functions
CodeWithLuwam May 9, 2023
25df3ee
Merge pull request #2 from luwammikael/wave-1
CodeWithLuwam May 9, 2023
763f291
completes DELETE one task function
CodeWithLuwam May 9, 2023
36e673e
Merge pull request #3 from luwammikael/wave-1
CodeWithLuwam May 9, 2023
762bea8
passing wave 1
CodeWithLuwam May 10, 2023
3d2c06a
Merge pull request #4 from luwammikael/wave-1
CodeWithLuwam May 10, 2023
3891dd7
passing wave2
CodeWithLuwam May 10, 2023
7c4d4e3
Merge pull request #5 from luwammikael/wave2
CodeWithLuwam May 10, 2023
a669c46
passing test_mark_complete
CodeWithLuwam May 10, 2023
a0d7368
Merge pull request #6 from luwammikael/wave3
CodeWithLuwam May 10, 2023
2d53b43
passing mark_incomplete
CodeWithLuwam May 10, 2023
70e6fa7
passing wave 3
CodeWithLuwam May 10, 2023
e8c5c77
Merge pull request #7 from luwammikael/wave3
CodeWithLuwam May 10, 2023
bc2f340
catching local main upto local wave3
CodeWithLuwam May 10, 2023
1aee551
passing wave4
CodeWithLuwam May 12, 2023
8d19974
wave5 2 tests passing
CodeWithLuwam May 12, 2023
b1fb43e
Merge pull request #8 from luwammikael/wave5
CodeWithLuwam May 12, 2023
9d50edf
passing wave5
CodeWithLuwam May 12, 2023
00c3a4f
passing wave5 Merge branch 'wave5'
CodeWithLuwam May 12, 2023
68bf2b1
passing all waves
CodeWithLuwam May 14, 2023
80324b3
Merge pull request #9 from luwammikael/wave6
CodeWithLuwam May 14, 2023
c66a0b8
Connect to Render db
CodeWithLuwam May 23, 2023
3dd77c8
changes the database environment variable name from Renderbase_URL to…
CodeWithLuwam Sep 7, 2023
11d2442
adds web:flask run to Procfile
CodeWithLuwam Sep 7, 2023
df2b6bb
changes procfile
CodeWithLuwam Sep 7, 2023
254a86a
everytime it deploys it will upgrade data base with release flask db …
CodeWithLuwam Sep 7, 2023
b7af79c
changes URI to URL
CodeWithLuwam Sep 7, 2023
00a0c2d
adds flask cors
CodeWithLuwam Sep 8, 2023
7287e00
adds import cors and calls app with cors
CodeWithLuwam Sep 8, 2023
d747a54
goal_routes minor spelling change
CodeWithLuwam Aug 17, 2024
e4216fc
minor chane in comment
CodeWithLuwam Aug 17, 2024
a342052
instructions via comments
CodeWithLuwam Aug 17, 2024
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
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
release: flask db upgrade
web: gunicorn "app:create_app()"
13 changes: 9 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
from flask_cors import CORS


db = SQLAlchemy()
Expand All @@ -12,15 +13,14 @@

def create_app(test_config=None):
app = Flask(__name__)
CORS(app)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URL")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( "SQLALCHEMY_TEST_DATABASE_URI")

# Import models here for Alembic setup
from app.models.task import Task
Expand All @@ -30,5 +30,10 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .task_routes import tasks_bp
app.register_blueprint(tasks_bp)

from .goal_routes import goals_bp
app.register_blueprint(goals_bp)
Comment on lines +33 to +37

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice choice to split up the routes into files that are more specific to the resources they work with!

Based on the paths to the files, it looks like the route files are directly in the app folder. Since we have more than one route file, I'd suggest creating a routes directory to organize those route files in.


return app
90 changes: 90 additions & 0 deletions app/goal_routes.py

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, really nice use of descriptive names and spacing to make the code easy to read!

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import os
import requests
from flask import Blueprint, jsonify, make_response, request
from app import db
from app.models.goal import Goal
from app.models.task import Task
from app.helper import validate_goal, validate_task

#CREATE BP/ENDPOINT
goals_bp = Blueprint("goals", __name__, url_prefix="/goals")

# GET all goals - GET[READ] - /goals
@goals_bp.route("", methods =["GET"])
def get_all_goals():
# if request.args.get("sort") == "asc":
# goals = Goal.query.order_by(Goal.title.asc())
# elif request.args.get("sort") == "desc":
# goals = Goal.query.order_by(Goal.title.desc())
# else:
Comment on lines +15 to +19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to remove commented code like this before opening PRs and rely on our repo's commit history if we need to look up past code.

goals = Goal.query.all()
goals_response = []
for goal in goals:
goals_response.append(goal.to_json())
Comment on lines +21 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a great place for a list comprehension:

goals_response = [goal.to_json() for goal in goals]

Are there other places in the project that we fill a list where we could use list comprehensions?


return jsonify(goals_response), 200

# GET one goal - /goals/<id> - [READ]
@goals_bp.route("/<id>", methods=["GET"])
def get_one_goal(id):
goal = validate_goal(id)
return jsonify({"goal":goal.to_json()}), 200

#POST - /goals - [CREATE]
@goals_bp.route("", methods= ["POST"])
def create_goal():
request_body = request.get_json()
new_goal = Goal.create_dict(request_body)
db.session.add(new_goal)
db.session.commit()
return make_response({"goal":new_goal.to_json()}), 201

#UPDATE one goal- PUT /goals/<id> [UPDATE]
@goals_bp.route("/<id>",methods=["PUT"])
def update_goal(id):
goal = validate_goal(id)
request_body = request.get_json()
goal.update_dict(request_body)
db.session.commit()

return jsonify({"goal":goal.to_json()}), 200

#DELETE one goal -DELETE /goals/<id> [DELETE]
@goals_bp.route("/<id>", methods=["DELETE"])
def delete_goal(id):
goal_to_delete = validate_goal(id)

db.session.delete(goal_to_delete)
db.session.commit()

message = {"details": f'Goal 1 "{goal_to_delete.title}" successfully deleted'}
return make_response(message, 200)

#POST tasks ids to goal /goals/1/tasks [CREATE]
@goals_bp.route("/<id>/tasks", methods=["POST"])
def post_task_ids_to_goal(id):
goal = validate_goal(id)
request_body = request.get_json()

validated_task = []
[validated_task.append(validate_task(task_id)) for task_id in request_body["task_ids"]]
Comment on lines +69 to +70

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line might not be doing what you intend, line 70 fills validated_task because of the statement validated_task.append(validate_task(task_id)), but it also creates a new list that we don't keep. we could re-write this line something like:

validated_task = [validate_task(task_id) for task_id in request_body["task_ids"]]

This line is a bit long, so we could create a variable to get the task_ids from the request_body:

task_ids = request_body["task_ids"]
validated_task = [validate_task(task_id) for task_id in task_ids]

This feedback around creating a list that we don't end up using by wrapping the line in square brackets applies to line 75 below as well. How might we re-write that line?

# for task_id in request_body["task_ids"]:
# task = validate_task(task_id)
# validated_task.append(task)

[goal.tasks.append(task) for task in validated_task if task not in goal.tasks]
# for task in validated_task:
# if task not in goal.tasks:
# goal.tasks.append(task)

db.session.commit()


return make_response({"id" : goal.goal_id, "task_ids":request_body["task_ids"]}), 200

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some lines across the files that are a bit long for best practices, how could we split them up?


# GET one goal - /goals/<id> - [READ]
@goals_bp.route("/<id>/tasks", methods=["GET"])
def get_tasks_in_one_goal(id):
goal = validate_goal(id)
goal_with_tasks = [Task.to_json(task) for task in goal.tasks]
return jsonify({"id":goal.goal_id, "title":goal.title, "tasks":goal_with_tasks}), 200
31 changes: 31 additions & 0 deletions app/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from flask import abort, make_response
from app.models.goal import Goal
from app.models.task import Task

def validate_task(id):
try:
id = int(id)
except:
abort(make_response({"message": f"Task {id} is invalid"}, 400))

task = Task.query.get(id)

if not task:
abort(make_response({"message": f"Task {id} not found"}, 404))

return task


def validate_goal(id):
try:
id = int(id)
except:
abort(make_response({"message": f"Goal {id} is invalid"}, 400))

goal = Goal.query.get(id)

if not goal:
abort(make_response({"message": f"Goal {id} not found"}, 404))

return goal
Comment on lines +5 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of similar code between these functions, how could we D.R.Y. up our code?


50 changes: 49 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
from app import db
from flask import abort, make_response, jsonify


# Define the Goal class: it represents a goal in the database
class Goal(db.Model):
# Define the goal_id column as the primary key for the Goal table
goal_id = db.Column(db.Integer, primary_key=True)

# Define the title column as a string that cannot be null
title = db.Column(db.String, nullable = False)


# Establish a one-to-many relationship with the Task model(many tasks)
# The `tasks` attribute holds a list of Task objects related to this Goal
# `back_populates` refers to the "goal" relationship defined in the Task model
tasks = db.relationship("Task", back_populates="goal", lazy=True)


# Convert a Goal object to a JSON-serializable dictionary
def to_json(self):
return{
"id": self.goal_id,
"title": self.title
}

# Update the Goal object's attributes based on the provided request body
def update_dict(self, request_body):
self.title = request_body["title"]


# @classmethod decorator indicates that create_dict is a class method associated
# with the class itself rather than any particular instance of the class

# cls parameter: a reference to the class (Goal in this case) and is used
# to call the class’s constructor (cls(...)), allowing the method to create a new instance of the class.

# create_dict method is designed to create a new Goal object using data provided in a dictionary (response_body).
@classmethod
# When the create_dict method creates a new Goal object, it does so by calling the class constructor (cls(...))
def create_dict(cls, response_body):
try:
# Create a new Goal object by calling the class constructor (cls(...))
# Extract the "title" value from response_body and pass it to the Goal constructor
# to set the title attribute of the new Goal instance.
new_goal = cls(
title = response_body["title"]
)
except KeyError:
# If the "title" key is missing from the response body, return a 400 error
abort(make_response(jsonify({"details": "Invalid data"}), 400))
return new_goal


48 changes: 46 additions & 2 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
from app import db

from flask import abort, make_response, jsonify
from datetime import datetime

class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.Integer, primary_key=True, autoincrement = True)
title = db.Column(db.String, nullable = False)
description = db.Column(db.String, nullable = False )
completed_at = db.Column(db.DateTime, default = None, nullable = True)
#child - many to one (the task are the children/ many children(tasks) to one parent(goal))
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"), nullable=True)
goal = db.relationship("Goal", back_populates="tasks")


def to_json(self):
is_complete = True if self.completed_at else False;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great choice to derive the value of is_complete from self.completed_at! We could also use the python bool function here instead of a ternary expression, what could that look like?


task_return = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": is_complete
}

if self.goal_id:
task_return["goal_id"] = self.goal_id

return task_return

def update_dict(self, request_body):
self.title = request_body["title"]
self.description = request_body["description"]

def patch_complete(self, request_body):
self.completed_at = datetime.utcnow()

def patch_incomplete(self,request_body):
self.completed_at = None

@classmethod
def create_dict(cls, response_body):
try:
new_task = cls(
title = response_body["title"],
description = response_body["description"]
)
except KeyError:
abort(make_response(jsonify({"details": "Invalid data"}), 400))
return new_task
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

95 changes: 95 additions & 0 deletions app/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os, requests
from flask import Blueprint, jsonify, make_response, request
from app import db
from app.models.task import Task
from app.helper import validate_task

#CREATE BP/ENDPOINT
tasks_bp = Blueprint("tasks", __name__, url_prefix= "/tasks")

# GET all tasks - /tasks [READ]
@tasks_bp.route("", methods =["GET"])
def get_all_tasks():
if request.args.get("sort") == "asc":
tasks = Task.query.order_by(Task.title.asc())
elif request.args.get("sort") == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()
tasks_response = []
for task in tasks:
tasks_response.append(task.to_json())

return jsonify(tasks_response), 200

# GET one task - /tasks/<id> [READ]
@tasks_bp.route("/<id>", methods=["GET"])
def get_one_task(id):
task = validate_task(id)
return jsonify({"task":task.to_json()}), 200

#POST - /tasks [CREATE]
@tasks_bp.route("", methods= ["POST"])
def create_task():
request_body = request.get_json()
new_task = Task.create_dict(request_body)

db.session.add(new_task)
db.session.commit()

return make_response({"task":new_task.to_json()}), 201

#PUT one task - /tasks/<id> [UPDATE]
@tasks_bp.route("/<id>",methods=["PUT"])
def update_task(id):
task = validate_task(id)
request_body = request.get_json()
task.update_dict(request_body)

db.session.commit()

return jsonify({"task":task.to_json()}), 200

#DELETE one task - /tasks/<id> [DELETE]
@tasks_bp.route("/<id>", methods=["DELETE"])
def delete_task(id):
task_to_delete = validate_task(id)

db.session.delete(task_to_delete)
db.session.commit()

message = {"details": f'Task 1 "{task_to_delete.title}" successfully deleted'}
return make_response(message, 200)

#PATCH /tasks/1/mark_incomplete [UPDATE]
@tasks_bp.route("/<id>/mark_incomplete", methods = ["PATCH"])
def mark_incompleted(id):
task_to_incomplete = validate_task(id)
request_body = request.get_json()
task_to_incomplete.patch_incomplete(request_body)

db.session.commit()

return jsonify({"task":task_to_incomplete.to_json()}), 200

# PATCH tasks/<id>/mark_complete [UPDATE]
@tasks_bp.route("/<id>/mark_complete", methods=["PATCH"])
def mark_completed(id):
task_to_complete = validate_task(id)
request_body = request.get_json()
task_to_complete.patch_complete(request_body)

db.session.commit()

PATH = "https://slack.com/api/chat.postMessage"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PATH is a local variable of the mark_completed function, so we'd typically see their name lowercased here. We use all caps for constant values, typically when declared where they're accessible across a file or project.


query_params = {
"channel": "api-test-channel",
"text": f'Task {task_to_complete.title} has been completed!'
}

the_headers = {"Authorization": os.environ.get("SLACK_API_KEY")}

requests.post(PATH, params=query_params, headers=the_headers)

return jsonify({"task":task_to_complete.to_json()}), 200
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
Loading