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

Mythical-Mycology-Muse-Wilson-Dodson-Edwards-Joseph-C19 #20

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2af393e
created basics for models and commented out the one-many relationship…
lindsaybolz Jun 26, 2023
110a931
Created database and made first migration
lindsaybolz Jun 26, 2023
9d49a45
removed migrations
stacyyyd Jun 26, 2023
4f17264
third times the charm
lindsaybolz Jun 26, 2023
e191334
Added board and card to init file
lindsaybolz Jun 26, 2023
249808e
Added create and get boards
stacyyyd Jun 26, 2023
2a773a9
added create and read cards from a board
lindsaybolz Jun 26, 2023
3a0612d
Added the delete for cards
lindsaybolz Jun 26, 2023
2ac73b6
Created tests for posting board
stacyyyd Jun 27, 2023
0370056
Added tests for get all boards
stacyyyd Jun 27, 2023
d452f52
Created tests for getting cards from board
stacyyyd Jun 27, 2023
7d9b8b8
Added tests for validate_model
stacyyyd Jun 27, 2023
a060ada
Possible new routes at bottom of routes
lindsaybolz Jun 27, 2023
729a35e
added likes attribute to card model
lindsaybolz Jun 28, 2023
8922396
Added an update likes route and fixed tests but have not yet made tes…
lindsaybolz Jun 28, 2023
13ee2e5
Added tests for updating/incrementing the card likes
lindsaybolz Jun 28, 2023
d375e47
added slack update when card is created
lindsaybolz Jun 28, 2023
44f0f8e
added slack update cards and tests for that
lindsaybolz Jun 28, 2023
2dc9261
added documentation on routes available
lindsaybolz Jun 28, 2023
73440df
added render-database-uri
lindsaybolz Jun 28, 2023
2604abe
Added delete route to board, changed post board return to return the …
stacyyyd Jul 17, 2023
11a29bb
Updated formatting to be compatable with json on card and board post …
lindsaybolz Jul 17, 2023
2c63d38
Updated documentation
lindsaybolz Jul 20, 2023
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
54 changes: 54 additions & 0 deletions OUR_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Mystical Mycology Muse

The Mystical Mycology Muse server is to be used with the [M^3 Dashboard](https://github.com/lindsaybolz/front-end-inspiration-board)

## Quick Start

1. Clone this repository. **You do not need to fork it first.**
- `git clone https://github.com/lindsaybolz/back-end-inspiration-board.git`

1. Create and activate a virtual environment
- `python3 -m venv venv`
- `source venv/bin/activate`
1. Install the `requirements.txt`
- `pip install -r requirements.txt`
1. Create a `.env` file with your API keys
```bash
# .env

# SQLALCHEMY_DATABASE_URI
SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://postgres:postgres@localhost:5432/inspiration_board_development

# SQLALCHEMY_TEST_DATABASE_URI
SQLALCHEMY_TEST_DATABASE_URI=postgresql+psycopg2://postgres:postgres@localhost:5432/inspiration_board_test

# SLACK_API_KEY
SLACK_API_KEY=Bearer "replace_with_your_api_key"
```
1. Create Databases on personal psql:
```
$ psql -U postgres
> CREATE DATABASE inspiration_board_test;
> CREATE DATABASE inspiration_board_development;
```
2. Create flask database connection
```
$ flask db init
$ flask db migrate
$ flask db upgrade
```
3. Run the server
- `flask run`

## Endpoints

| Route | Query Parameter(s) | Query Parameter(s) Description |
|--|--|--|
|`POST` `/boards`| `owner` & `title` | Owner and title of board strings in json ex: `{"id": 1, "owner": "Lindsay", "title": "Fungi's 101"}` |
|`GET` `/boards` | None | Returns list of boards as dictionaries `[{"id": 1, "owner": "Lindsay", "title": "Fungi's 101"}, {...}]`|
|`DELETE` `/boards/<board_id>` | None | Returns: `Board successfully deleted`|
|`POST` `/boards/<board_id>/cards` | `message` | Returns: `{"id": 1 "owner": Stacy "title": "Fungi's 101"}`|
|`GET` `/boards/<board_id>/cards` | None | Returns list of cards for `<board_id>`: `[{'id': 1, message': "I like fungi" 'likes': 1 'board': "Fungi's 101"}, {...}]`. This also posts a message to slack.|
|`DELETE` `/cards/<card_id>` | None | Returns: `Card successfully deleted`|
|`PATCH` `/cards/<card_id>` | None | Returns: dictionary of the card that was changed with updated like count by +1: `{'id': 1,'message': "I like fungi" 'likes': 2, 'board': "Fungi's 101"}`|

19 changes: 14 additions & 5 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,31 @@
load_dotenv()


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

app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
if not test_config:
#app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("RENDER_DATABASE_URI")
else:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")


# Import models here for Alembic setup
# from app.models.ExampleModel import ExampleModel
from app.models.board import Board
from app.models.card import Card

db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here
# from .routes import example_bp
# app.register_blueprint(example_bp)
from .routes import boards_bp, cards_bp
app.register_blueprint(boards_bp)
app.register_blueprint(cards_bp)


CORS(app)
return app
17 changes: 17 additions & 0 deletions app/models/board.py
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
from app import db

class Board(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
owner = db.Column(db.String)
title = db.Column(db.String)
cards = db.relationship("Card", back_populates='board',cascade = "all, delete-orphan")

def to_dict(self):
return {
"id": self.id,
"owner": self.owner,
"title": self.title
}

@classmethod
def from_dict(cls, dict_data):
return Board(owner = dict_data['owner'], title = dict_data['title'])
15 changes: 15 additions & 0 deletions app/models/card.py
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
from app import db

class Card(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
message = db.Column(db.String)
likes = db.Column(db.Integer, default=0)
board_id = db.Column(db.Integer, db.ForeignKey('board.id'))
board = db.relationship("Board", back_populates='cards')

def to_dict(self):
return {
'id': self.id,
'message': self.message,
'likes': self.likes,
'board': self.board.title,
}
120 changes: 118 additions & 2 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,120 @@
from flask import Blueprint, request, jsonify, make_response
from flask import Blueprint, request, jsonify, abort, make_response
from app import db
from app.models.board import Board
from app.models.card import Card
import requests
import os

# example_bp = Blueprint('example_bp', __name__)

# Helper Functions:
def validate_model(model_class, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({'message': f"{model_id} is not a valid type. It must be an integer"}, 400))

model = model_class.query.get(model_id)

if not model:
abort(make_response({'message': f'{model_id} does not exist'}, 404))

return model

def post_to_slack(card):
path = 'https://slack.com/api/chat.postMessage'
header = {"Authorization": os.environ.get("SLACK_API_KEY")}
data = {
'channel': 'new-card-updates',
'text': f"Card with message \"{card.message}\" for board \"{card.board.title}\" was created!"
}
response = requests.post(path, headers=header, data=data)
return response

#example_bp = Blueprint('example_bp', __name__)
boards_bp = Blueprint("boards", __name__, url_prefix="/boards")
cards_bp = Blueprint("cards", __name__, url_prefix="/cards")

# create a board
@boards_bp.route("", methods=["POST"])
def create_board():
request_body = request.get_json()
try:
if request_body.get('owner') and request_body.get('title'):
new_board = Board.from_dict(request_body)

db.session.add(new_board)
db.session.commit()

return jsonify(new_board.to_dict()), 201
except:
abort(make_response({"message": "Board input data incomplete"}, 400))



# get all boards
@boards_bp.route("", methods = ["GET"])
def get_boards():
boards = Board.query.all()

board_response = []
for board in boards:
board_response.append(board.to_dict())

return jsonify(board_response), 200

# delete board
@boards_bp.route('/<board_id>', methods=['DELETE'])
def delete_board(board_id):
board = validate_model(Board, board_id)

db.session.delete(board)
db.session.commit()

return jsonify('Board successfully deleted'), 201

# Create a new card for the selected board
@boards_bp.route("/<board_id>/cards", methods=["POST"])
def create_card_for_board(board_id):
board = validate_model(Board, board_id)
request_body = request.get_json()
if len(request_body['message']) > 40:
abort(make_response({"message": "Message was too long, keep it under 40 characters please"}, 400))

new_card = Card(message=request_body['message'], board=board)

db.session.add(new_card)
db.session.commit()

post_to_slack(new_card)

return jsonify(new_card.to_dict()), 201

# get cards for board_id
@boards_bp.route("/<board_id>/cards", methods=["GET"])
def get_cards_for_board(board_id):
board = validate_model(Board, board_id)
cards = []
for card in board.cards:
cards.append(card.to_dict())

return jsonify(cards), 200

# delete card
@cards_bp.route('/<card_id>', methods=['DELETE'])
def delete_card(card_id):
card = validate_model(Card, card_id)

db.session.delete(card)
db.session.commit()

return jsonify('Card successfully deleted'), 201

# Updating like by +1 on card
@cards_bp.route('/<card_id>', methods=['PATCH'])
def increment_like_on_card(card_id):
card = validate_model(Card, card_id)

card.likes = card.likes + 1
db.session.commit()

return card.to_dict(), 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.
45 changes: 45 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# A generic, single database configuration.

[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
96 changes: 96 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import with_statement

import logging
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool
from flask import current_app

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline():
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""

# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')

connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Loading