diff --git a/.gitignore b/.gitignore index cb14e7f..b7da395 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store venv/ +.vscode/ +__pycache__/ \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..ca6e941 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn app:app diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ccc7c4 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# capstone + +Link to proposal: + +https://docs.google.com/document/d/1vihomjFiPxAEcT1a_XN5z64aQJ3TPLc6ZatwUhQOaoU/edit?usp=sharing + + +Title of my site: MTG Deck Builder +Link to the URL where it is deployed: https://mtg-deck-builder-herokuapp.herokuapp.com/ + +MTG Deck Builder allows users to search for cards in the MTG library, see info on the cards, create their own decks, create posts, add friends, and view decks that other users have made. + +Features implemented: +- The search bar at the top allows users to search for cards by card name, decks by deck name or type, and users/friends by username. This is what I believe would be the most common search criteria. +- There is AJAX involved in bookmarking a card, adding/removing a friend and adding a card to a deck - this way, a user does have to leave the page or refresh and can keep their momentum going through the site. +- You are able to see the owner of any given deck and click their username to immediately view their profile - this allows users to easily add a friend who has a deck they like, or simply view other decks from that user by visiting their profile. +- When you click "Add to Deck" under a card, at the end of the list of your decks that pops up under the card there is a button that says "Create Deck" - this allows you to add the selected card to a brand new deck. Upon making the new deck, the selected card will automatically be added to that deck. + +Standard user flow: +1. Login/Register +2. View home page where all cards (paginated) are shown +3. Create new deck +4. Search for cards by name +5. Add cards to decks +6. You may also search for other decks for inspiration + +Notes on MTG API: +- Requests for cards can take a very long time - if you want to retrieve every single card from the API, it can take around 2 hours. Also, the Python SDK suddenly stopped working one day, so there seems to be some issues that need fixing. + +Technology stack used: +- astroid==2.4.2 +- autopep8==1.5.4 +- bcrypt==3.2.0 +- blinker==1.4 +- certifi==2020.6.20 +- cffi==1.14.3 +- chardet==3.0.4 +- click==7.1.2 +- dnspython==2.0.0 +- email-validator==1.1.1 +- Flask==1.1.2 +- Flask-Bcrypt==0.7.1 +- Flask-DebugToolbar==0.11.0 +- flask-paginate==0.7.1 +- Flask-SQLAlchemy==2.4.4 +- Flask-WTF==0.14.3 +- idna==2.10 +- isort==5.6.4 +- itsdangerous==1.1.0 +- Jinja2==2.11.2 +- lazy-object-proxy==1.4.3 +- MarkupSafe==1.1.1 +- mccabe==0.6.1 +- mtgsdk==1.3.1 +- psycopg2-binary==2.8.6 +- pycodestyle==2.6.0 +- pycparser==2.20 +- pylint==2.6.0 +- requests==2.24.0 +- six==1.15.0 +- SQLAlchemy==1.3.20 +- toml==0.10.1 +- typed-ast==1.4.1 +- urllib3==1.25.11 +- Werkzeug==1.0.1 +- wrapt==1.12.1 +- WTForms==2.3.3 + +There are still a lot of features that I want to add, but I need to move on with the curriculum so I am submitting my project with the features it currently has and will add more features as stretch goals. diff --git a/app.py b/app.py index d0b025a..9bfe526 100644 --- a/app.py +++ b/app.py @@ -34,8 +34,6 @@ CURR_USER_KEY = 'curr-user' -TYPES = mtgsdk.Type.all() - @app.before_request def add_user_to_g(): @@ -44,6 +42,3 @@ def add_user_to_g(): g.user = User.query.get(session[CURR_USER_KEY]) else: g.user = None - - -print('***********') diff --git a/bookmarks.py b/bookmarks.py index ab6d0c6..224ce52 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -9,20 +9,20 @@ from app import g from flask import Flask, session, Blueprint, request, render_template, redirect, flash from flask_debugtoolbar import DebugToolbarExtension -from models import db, connect_db, User, Friendship, Message, Card, Bookmark, Deck, CardDeck, Post -from forms import LoginForm, RegisterForm, TypeForm, PowerForm, ToughnessForm, DeckForm, EditUserForm +from models import db, connect_db, User, Friendship, Card, Bookmark, Deck, CardDeck, Post +from forms import LoginForm, RegisterForm, DeckForm, EditUserForm bookmarks_blueprint = Blueprint('bookmarks_blueprint', __name__, static_folder='static', template_folder='templates') CURR_USER_KEY = 'curr-user' -TYPES = mtgsdk.Type.all() - @bookmarks_blueprint.route('/cards//bookmark', methods=['GET', 'POST']) def add_bookmark(card_id): + """Route for bookmarking a card""" if g.user: + """If this is a post request, create a new bookmark instance""" bookmark = Bookmark(card_id=card_id, username=session[CURR_USER_KEY]) db.session.add(bookmark) db.session.commit() @@ -33,6 +33,7 @@ def add_bookmark(card_id): @bookmarks_blueprint.route('/cards//unbookmark', methods=['GET', 'POST']) def remove_bookmark(card_id): + """Route for unbookmarking a card""" if g.user: bookmark = Bookmark.query.filter(Bookmark.card_id == card_id).first() db.session.delete(bookmark) @@ -44,17 +45,13 @@ def remove_bookmark(card_id): @bookmarks_blueprint.route('/bookmarks') def show_bookmarked_cards(): + """Route for showing your bookmarked cards""" if g.user: bookmarked_cards = g.user.bookmarked_cards decks = Deck.query.all() bookmarked_card_ids = [ bookmarked_card.id for bookmarked_card in bookmarked_cards] - type_form = TypeForm() - type_form.card_type.choices = TYPES - - power_form = PowerForm() - toughness_form = ToughnessForm() - return render_template('bookmarks.html', bookmarked_cards=bookmarked_cards, decks=decks, type_form=type_form, power_form=power_form, toughness_form=toughness_form, bookmarked_card_ids=bookmarked_card_ids) + return render_template('bookmarks.html', bookmarked_cards=bookmarked_cards, decks=decks, bookmarked_card_ids=bookmarked_card_ids) return redirect('/login') diff --git a/decks.py b/decks.py index 11ca689..81d3b48 100644 --- a/decks.py +++ b/decks.py @@ -9,55 +9,58 @@ from app import g from flask import Flask, Blueprint, session, request, render_template, redirect, flash from flask_debugtoolbar import DebugToolbarExtension -from models import db, connect_db, User, Friendship, Message, Card, Bookmark, Deck, CardDeck, Post -from forms import LoginForm, RegisterForm, TypeForm, PowerForm, ToughnessForm, DeckForm, EditUserForm +from models import db, connect_db, User, Card, Bookmark, Deck, CardDeck +from forms import DeckForm decks_blueprint = Blueprint('decks_blueprint', __name__, static_folder='static', template_folder='templates') CURR_USER_KEY = 'curr-user' -TYPES = mtgsdk.Type.all() - @decks_blueprint.route('/decks', methods=['GET', 'POST']) def view_decks(): + """Route for viewing your own decks""" if g.user: + user = g.user decks = g.user.decks - return render_template('decks.html', decks=decks) + search = False + return render_template('decks.html', decks=decks, user=user, search=search) + return redirect('/login') @decks_blueprint.route('/decks/') def show_deck(deck_id): - deck = Deck.query.get(deck_id) - - bookmarks = Bookmark.query.all() - bookmarked_card_ids = [bookmark.card_id for bookmark in bookmarks] - type_form = TypeForm() - type_form.card_type.choices = TYPES + """Route for viewing contents of deck""" + if g.user: + deck = Deck.query.get(deck_id) - power_form = PowerForm() - toughness_form = ToughnessForm() - return render_template('deck.html', deck=deck, type_form=type_form, power_form=power_form, toughness_form=toughness_form, bookmarked_card_ids=bookmarked_card_ids) + bookmarks = Bookmark.query.all() + bookmarked_card_ids = [bookmark.card_id for bookmark in bookmarks] + return render_template('deck.html', deck=deck, bookmarked_card_ids=bookmarked_card_ids) + return redirect('/login') @decks_blueprint.route('/decks//delete', methods=['POST']) def delete_deck(deck_id): - deck = Deck.query.get(deck_id) - db.session.delete(deck) - db.session.commit() - return redirect('/decks') + """Route for deleting a deck""" + if g.user: + deck = Deck.query.get(deck_id) + db.session.delete(deck) + db.session.commit() + return redirect('/decks') + return redirect('/login') @decks_blueprint.route('/new', methods=['GET', 'POST']) def create_deck(): - + """Route for creating a deck""" if g.user: form = DeckForm() form.deck_type.choices = ['Standard', 'Commander'] if form.validate_on_submit(): - + """If this is a post request, created a new deck instance""" deck = Deck(deck_name=form.deck_name.data, deck_type=form.deck_type.data, username=session[CURR_USER_KEY]) @@ -71,11 +74,12 @@ def create_deck(): return redirect('/decks') return render_template('new_deck.html', form=form) - return redirect('/') + return redirect('/login') @decks_blueprint.route('/cards//decks/', methods=['POST']) def add_to_deck(card_id, deck_id): + """Route for adding a card to your deck""" if g.user: card = Card.query.get(card_id) deck = Deck.query.get(deck_id) @@ -84,10 +88,12 @@ def add_to_deck(card_id, deck_id): db.session.commit() return redirect('/home') + return redirect('/login') @decks_blueprint.route('/cards//decks//delete', methods=['POST']) def delete_from_deck(card_id, deck_id): + """Route for deleting a card from your deck""" if g.user: card_deck = CardDeck.query.filter( CardDeck.card_id == card_id and CardDeck.deck_id == deck_id).first() @@ -96,10 +102,13 @@ def delete_from_deck(card_id, deck_id): db.session.commit() return redirect(f'/decks/{deck_id}') + return redirect('/login') @decks_blueprint.route('/users//decks') def show_users_decks(username): + """Route for viewing someone else's decks""" user = User.query.get(username) decks = user.decks - return render_template('decks.html', decks=decks) + search = False + return render_template('decks.html', decks=decks, user=user, search=search) diff --git a/forms.py b/forms.py index dc09460..e732d20 100644 --- a/forms.py +++ b/forms.py @@ -1,7 +1,8 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, IntegerField, RadioField, SelectField, FileField -from wtforms.validators import InputRequired, Length +from wtforms import StringField, PasswordField, IntegerField, RadioField, SelectField, FileField, TextAreaField +from wtforms.validators import InputRequired, Length, Email from wtforms.widgets import CheckboxInput, ListWidget +import email_validator class LoginForm(FlaskForm): @@ -11,7 +12,8 @@ class LoginForm(FlaskForm): class RegisterForm(FlaskForm): - email = StringField('Email', validators=[InputRequired()]) + email = StringField('Email', validators=[ + InputRequired(), Email("Invalid email address")]) username = StringField("Username", validators=[InputRequired()]) password = PasswordField("Password", validators=[ InputRequired(), Length(min=8)]) @@ -21,7 +23,8 @@ class RegisterForm(FlaskForm): class EditUserForm(FlaskForm): - email = StringField('Email', validators=[InputRequired()]) + email = StringField('Email', validators=[ + InputRequired(), Email("Invalid email address")]) password = PasswordField("Password", validators=[ InputRequired(), Length(min=8)]) confirmed_password = PasswordField( @@ -34,30 +37,8 @@ class DeckForm(FlaskForm): deck_type = SelectField('Deck Type') -class TypeForm(FlaskForm): - card_type = RadioField('Type', option_widget=CheckboxInput()) - - -class ColorForm(FlaskForm): - color = RadioField('Color', option_widget=CheckboxInput()) - - -class RarityForm(FlaskForm): - rarity = RadioField('Rarity', option_widget=CheckboxInput()) - - -class SetForm(FlaskForm): - set_name = RadioField('Set Name', option_widget=CheckboxInput()) - - -class PowerForm(FlaskForm): - power_conditionals = RadioField('Power', option_widget=CheckboxInput(), - choices=["Less than", "Equal to", "Greater than"]) - power = IntegerField('Power', default=0) - - -class ToughnessForm(FlaskForm): - - toughness_conditionals = RadioField('Toughness', option_widget=CheckboxInput(), - choices=["Less than", "Equal to", "Greater than"]) - toughness = IntegerField('Toughness', default=0) +class NewPostForm(FlaskForm): + title = StringField('Title', validators=[ + InputRequired()]) + content = TextAreaField('Content', validators=[ + InputRequired()]) diff --git a/friends.py b/friends.py index 1ea9545..98a7c6e 100644 --- a/friends.py +++ b/friends.py @@ -9,40 +9,42 @@ from app import g from flask import Flask, Blueprint, session, request, render_template, redirect, flash from flask_debugtoolbar import DebugToolbarExtension -from models import db, connect_db, User, Friendship, Message, Card, Bookmark, Deck, CardDeck, Post -from forms import LoginForm, RegisterForm, TypeForm, PowerForm, ToughnessForm, DeckForm, EditUserForm +from models import db, connect_db, User, Friendship, Card, Bookmark, Deck, CardDeck, Post +from forms import LoginForm, RegisterForm, DeckForm, EditUserForm friends_blueprint = Blueprint('friends_blueprint', __name__, static_folder='static', template_folder='templates') CURR_USER_KEY = 'curr-user' -TYPES = mtgsdk.Type.all() - -@friends_blueprint.route('/friends') -def show_friends(): +@friends_blueprint.route('/users//friends') +def show_users_friends(username): + """Route for showing your friends""" if g.user: - friends = g.user.friends - return render_template('friends.html', friends=friends) + user = User.query.get(username) + friends = user.friends + return render_template('friends.html', friends=friends, user=user) return redirect('/login') @friends_blueprint.route('/add_friend/', methods=['POST']) def add_friend(friend_username): + """Route for adding a friend""" if g.user: friend = User.query.get(friend_username) g.user.friends.append(friend) db.session.commit() - return redirect('/friends') + return redirect(f'/users/{g.user.username}/friends') return redirect('/login') @friends_blueprint.route('/remove_friend/', methods=['POST']) def remove_friend(friend_username): + """Route for removing a friend""" if g.user: friend = User.query.get(friend_username) g.user.friends.remove(friend) db.session.commit() - return redirect('/friends') + return redirect(f'/users/{g.user.username}/friends') return redirect('/login') diff --git a/home.py b/home.py index 87ecda6..3e32819 100644 --- a/home.py +++ b/home.py @@ -4,43 +4,19 @@ import json import mtgsdk import flask_paginate +import math -# from mtgsdk import Type from app import g from flask import Flask, Blueprint, session, request, render_template, redirect, flash, jsonify from flask_debugtoolbar import DebugToolbarExtension -from models import db, connect_db, User, Friendship, Message, Card, Bookmark, Deck, CardDeck, Post -from forms import LoginForm, RegisterForm, TypeForm, PowerForm, ToughnessForm, DeckForm, EditUserForm, ColorForm, RarityForm, SetForm +from models import db, connect_db, User, Friendship, Card, Bookmark, Deck, CardDeck, Post +from forms import LoginForm, RegisterForm, DeckForm, EditUserForm home_blueprint = Blueprint('home_blueprint', __name__, static_folder='static', template_folder='templates') CURR_USER_KEY = 'curr-user' -TYPES = mtgsdk.Type.all() -SETS = [mtg_set.name for mtg_set in mtgsdk.Set.all()] -RARITIES = ['Common', 'Uncommon', 'Rare', 'Mythic Rare'] -COLORS = ['White', 'Blue', 'Black', 'Green', 'Red'] - - -def generate_forms(): - type_form = TypeForm() - type_form.card_type.choices = TYPES - - color_form = ColorForm() - color_form.color.choices = COLORS - - rarity_form = RarityForm() - rarity_form.rarity.choices = RARITIES - - set_form = SetForm() - set_form.set_name.choices = SETS - - power_form = PowerForm() - toughness_form = ToughnessForm() - - return [type_form, color_form, rarity_form, set_form, power_form, toughness_form] - @ home_blueprint.route('/') def welcome(): @@ -50,52 +26,64 @@ def welcome(): """ if not g.user: return render_template('welcome.html') - return redirect('/home') @ home_blueprint.route('/home') def show_homepage(): - # base_url = '/home?' - # page = determine_page(request.args) - # index_range = determine_index_range(page) - - # cards = Card.query.filter((Card.id + 1).in_(index_range)).all() - cards = Card.query.all() + """Route for showing the home page with nav bar and all cards paginated""" + if g.user: + base_url = '/home?' + page = determine_page(request.args) + index_range = determine_index_range(page) + all_cards = Card.query.all() + last_page = determine_last_page(all_cards) + cards = Card.query.filter((Card.id).in_(index_range)).all() - return render_homepage(cards) + return render_homepage(all_cards=all_cards, last_page=last_page, base_url=base_url, page=page, index_range=index_range, cards=cards) + return redirect('/login') @ home_blueprint.route('/home/search') def search(): - + """Route for showing all the cards/users/friends/decks resulting from a search""" term = request.args['term'] category = request.args['category'] if category == 'card': + return search_cards(term, category, request.args) + else: + return handle_category(category, term) - return render_card_search(term, category, request.args) - elif category == 'deck': +def handle_category(category, term): + """Executes the render_template function accordingly depending on the search category & term""" + keyword = category + if category == 'deck': + search = True + user = g.user decks = Deck.query.filter( (Deck.deck_name.ilike(f'%{term}%')) | ( Deck.deck_type.ilike(f'%{term}%'))).all() - return render_template('decks.html', decks=decks) - + if len(decks) == 0: + flash('No results found.', 'danger') + return render_template('decks.html', user=user, decks=decks, search=search) elif category == 'friend': friends = [friend for friend in g.user.friends if term.casefold() in friend.username.casefold()] - print(g.user.friends) - return render_template('friends.html', friends=friends) - + if len(friends) == 0: + flash('No results found.', 'danger') + return render_template('friends.html', friends=friends, user=g.user) elif category == 'user': users = [user for user in User.query.filter( User.username.ilike(f'%{term}%')).all()] + if len(users) == 0: + flash('No results found.', 'danger') return render_template('users.html', users=users) -def render_card_search(term, category, req_args): - +def search_cards(term, category, req_args): + """Executes the render_template function for cards specifically""" base_url = f'/home/search?term={term}&category={category}&' page = determine_page(request.args) @@ -104,61 +92,27 @@ def render_card_search(term, category, req_args): all_cards = Card.query.filter( Card.name.ilike(f'%{term}%')).all() + last_page = determine_last_page(all_cards) + cards = [card for card in all_cards if (all_cards.index( card) + 1) in index_range] + return render_homepage(all_cards=all_cards, last_page=last_page, base_url=base_url, page=page, index_range=index_range, cards=cards) - return render_homepage(base_url, page, index_range, cards) - -@home_blueprint.route('/home/filter') -def filter_cards(): - - types = generate_filter_terms('card_type', TYPES, request.args) - sets = generate_filter_terms('set_name', SETS, request.args) - colors = generate_filter_terms('colors', COLORS, request.args) - rarities = generate_filter_terms('card_type', RARITIES, request.args) - - base_url = f'/home/filter?card_type={types}&sets={sets}&colors={colors}&rarities={rarities}&' - - page = determine_page(request.args) - index_range = determine_index_range(page) - - filtered_cards = generate_filtered_cards( - types, sets, colors, rarities, index_range) - - return render_homepage(base_url, page, index_range, filtered_cards) - -# ADD base_url, page, index_range - - -def render_homepage(cards): +def render_homepage(all_cards, last_page, cards, base_url, page, index_range): + """Determines how the home page should be rendered depending on the results from previous functions""" decks = Deck.query.all() bookmarks = Bookmark.query.all() bookmarked_card_ids = [bookmark.card_id for bookmark in bookmarks] - forms = generate_forms() - - type_form = forms[0] - color_form = forms[1] - rarity_form = forms[2] - set_form = forms[3] - power_form = forms[4] - toughness_form = forms[5] - - return render_template('home.html', cards=cards, decks=decks, type_form=type_form, - power_form=power_form, toughness_form=toughness_form, color_form=color_form, rarity_form=rarity_form, - set_form=set_form, bookmarked_card_ids=bookmarked_card_ids) - - -def generate_filter_terms(category, default_terms, req_args): - terms = default_terms - if category in request.args and len(req_args[category]) > 0: - terms = req_args[category].split(',') - return terms + if len(cards) == 0: + flash('No results found.', 'danger') + return render_template('home.html', all_cards=all_cards, page=page, last_page=last_page, base_url=base_url, cards=cards, decks=decks, bookmarked_card_ids=bookmarked_card_ids) def determine_page(req_args): + """Determines which page of cards the user should be on""" page = 1 if 'page' in req_args: page = int(req_args['page']) @@ -166,14 +120,12 @@ def determine_page(req_args): def determine_index_range(page): + """Determines the range of cards that should be on this page""" first_card_index = ((page-1)*100) + 1 last_card_index = (page*100) + 1 index_range = range(first_card_index, last_card_index) return index_range -def generate_filtered_cards(types, sets, colors, rarities, index_range): - filtered_cards = Card.query.filter(Card.card_type.in_(types) & Card.set_name.in_( - sets) & Card.colors.in_(colors) & Card.rarity.in_(rarities)).all() - return [card for card in filtered_cards if ( - filtered_cards.index(card) + 1) in index_range] +def determine_last_page(cards): + return math.ceil(len(cards)/100) diff --git a/models.py b/models.py index 0fc8129..634bd15 100644 --- a/models.py +++ b/models.py @@ -37,8 +37,6 @@ class User(db.Model): friends = db.relationship('User', secondary='friendships', primaryjoin=( Friendship.user1_username == username), secondaryjoin=(Friendship.user2_username == username)) posts = db.relationship('Post', backref='user') - messages = db.relationship( - 'Message', backref='user') @classmethod def signup(cls, username, password, email, image_url): @@ -61,38 +59,20 @@ def authenticate(cls, username, password): return False -class Message(db.Model): - __tablename__ = 'messages' - - id = db.Column(db.Integer, autoincrement=True, primary_key=True) - content = db.Column(db.Text, nullable=False) - date_time = db.Column(db.DateTime, nullable=False, - default=datetime.utcnow) - username = db.Column(db.Text, db.ForeignKey('users.username')) - # conversation = db.relationship('Conversation', backref='messages') - - -# class Conversation(db.Model): -# __tablename__ = 'conversations' - -# id = db.Column(db.Integer, autoincrement=True, primary_key=True) -# message_id = db.Column(db.Integer, db.ForeignKey( -# 'messages.id')) - - class Card(db.Model): __tablename__ = 'cards' id = db.Column(db.Integer, autoincrement=True, primary_key=True) - name = db.Column(db.Text, nullable=False) - image_url = db.Column(db.Text) + name = db.Column(db.Text, nullable=False, index=True) + image_url = db.Column( + db.Text) text = db.Column(db.Text) - card_type = db.Column(db.Text, nullable=False) - power = db.Column(db.Integer or db.String) - toughness = db.Column(db.Integer) - colors = db.Column(db.Text, nullable=False) - rarity = db.Column(db.Text, nullable=False) - set_name = db.Column(db.Text, nullable=False) + card_type = db.Column(db.Text, nullable=False, index=True) + power = db.Column(db.Text or db.String) + toughness = db.Column(db.Text) + colors = db.Column(db.Text, nullable=False, index=True) + rarity = db.Column(db.Text, nullable=False, index=True) + set_name = db.Column(db.Text, nullable=False, index=True) users = db.relationship('User', secondary='bookmarks', backref='cards') decks = db.relationship('Deck', secondary='cards_decks', backref='cards') @@ -103,7 +83,8 @@ def create_all_cards(cls, cards): colors = ' '.join(card['colors']) - image_url = card.get('imageUrl', 'static/images/mtg_default.jpg') + image_url = card.get( + 'imageUrl', 'https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg') text = card.get('text') power = card.get('power') @@ -170,6 +151,7 @@ class Post(db.Model): id = db.Column(db.Integer, autoincrement=True, primary_key=True) username = db.Column(db.ForeignKey('users.username')) + title = db.Column(db.Text, nullable=False) content = db.Column(db.Text, nullable=False) date_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) diff --git a/requirements.txt b/requirements.txt index e61c199..dee4ca4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,38 @@ +astroid==2.4.2 +autopep8==1.5.4 +bcrypt==3.2.0 blinker==1.4 +certifi==2020.6.20 +cffi==1.14.3 +chardet==3.0.4 click==7.1.2 +dnspython==2.0.0 +email-validator==1.1.1 Flask==1.1.2 +Flask-Bcrypt==0.7.1 Flask-DebugToolbar==0.11.0 +flask-paginate==0.7.1 Flask-SQLAlchemy==2.4.4 +Flask-WTF==0.14.3 +gunicorn==20.0.4 +idna==2.10 +isort==5.6.4 itsdangerous==1.1.0 Jinja2==2.11.2 +lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 +mccabe==0.6.1 +mtgsdk==1.3.1 psycopg2-binary==2.8.6 +pycodestyle==2.6.0 +pycparser==2.20 +pylint==2.6.0 +requests==2.24.0 +six==1.15.0 SQLAlchemy==1.3.20 +toml==0.10.1 +typed-ast==1.4.1 +urllib3==1.25.11 Werkzeug==1.0.1 +wrapt==1.12.1 +WTForms==2.3.3 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..6f651a3 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.7.3 diff --git a/seed.py b/seed.py index bff15d3..cfcbe7e 100644 --- a/seed.py +++ b/seed.py @@ -1,23 +1,19 @@ import requests from app import db -from models import connect_db, Card +from models import connect_db, Card, User, Friendship, Deck, Bookmark, Post, CardDeck -# for page in range(1, 546): -resp = requests.get('http://api.magicthegathering.io/v1/cards', { - 'key': "$2a$10$TNyqKQQQSzVjgGXY87waZuBIKAS78.NkY2o.H004TfBU.eISv.Pt6" -}).json() -cards = resp['cards'] -# print(f'Page: {page}, Count: {len(cards)}') -Card.create_all_cards(cards) +print('STARTING SEED...') +db.drop_all() +db.create_all() -for page in range(1, 10): +for page in range(1, 40): + print(f"Page: {page}...") resp = requests.get('http://api.magicthegathering.io/v1/cards', { 'key': "$2a$10$TNyqKQQQSzVjgGXY87waZuBIKAS78.NkY2o.H004TfBU.eISv.Pt6", 'page': page }).json() cards = resp['cards'] - print('*****') - print(cards) Card.create_all_cards(cards) + print('** DONE **') db.session.commit() diff --git a/static/app.js b/static/app.js index ce20250..13ce4d2 100644 --- a/static/app.js +++ b/static/app.js @@ -1,145 +1,127 @@ -$("#filter-form").on("submit", async function (evt) { - // let power; - // $("#power-form:input").each(() => { - // if ($(this).is(":checked")) { - // let label = $("label[for='" + $(this).attr("id") + "']"); - // types.push(label); - // } - // }); +$('.bookmark-btn-form').each(function() { + let form = $(this)[0] + let card_id = form.id + $(form).on('submit', async function(evt) { + evt.preventDefault() + if ($(form).hasClass('bookmark-form')) { + axios.post(`/cards/${form.id}/bookmark`, { + 'card_id': card_id + }) + + $(form).empty() + $(form).attr('action', `/cards/${card_id}/unbookmark`); + $(form).removeClass('bookmark-form') + $(form).addClass('unbookmark-form') + $(form).append(``) + } else if ($(form).hasClass('unbookmark-form')) { + axios.post(`/cards/${form.id}/unbookmark`, { + 'card_id': card_id + }) + + $(form).empty() + $(form).attr('action', `/cards/${card_id}/bookmark`); + $(form).removeClass('unbookmark-form') + $(form).addClass('bookmark-form') + $(form).append(``) + } + }) +}) - // let toughness; - // $("#toughness-form:input").each(() => { - // if ($(this).is(":checked")) { - // let label = $("label[for='" + $(this).attr("id") + "']"); - // types.push(label); - // } - // }); +$('.friend-btn-form').each(function() { + let form = $(this)[0] + let username = form.id + $(form).on('submit', async function(evt) { + evt.preventDefault() + if ($(form).hasClass('add-friend-form')) { + axios.post(`/add_friend/${username}`, { + 'username': username + }) + + $(form).empty() + $(form).attr('action', `/remove_friend/${username}`); + $(form).removeClass('add-friend-form') + $(form).addClass('remove-friend-form') + $(form).append(``) + } else if ($(form).hasClass('remove-friend-form')) { + axios.post(`/remove_friend/${username}`, { + 'username': username + }) + + $(form).empty() + $(form).attr('action', `/add_friend/${username}`); + $(form).removeClass('remove-friend-form') + $(form).addClass('add-friend-form') + $(form).append(``) + } + }) +}) - evt.preventDefault(); +$('.add-to-deck-btn').each(function() { + let btn = $(this)[0] + $(btn).on('click', function(evt) { + evt.preventDefault() + toggleDropdownIcon(btn) + }) +}) - let types = []; - $("#type-form input").each(function () { - if ($(this).is(":checked")) { - let label = $("label[for='" + $(this).attr("id") + "']")[0]["innerText"]; - types.push(label); - } - }); +$('.show-info-btn').each(function() { + let btn = $(this)[0] + $(btn).on('click', function(evt) { + evt.preventDefault() + toggleDropdownIcon(btn) + }) +}) - let sets = []; - $("#set-form input").each(function () { - if ($(this).is(":checked")) { - let label = $("label[for='" + $(this).attr("id") + "']")[0]["innerText"]; - sets.push(label); - } - }); +function toggleDropdownIcon(btn) { + if ($(btn).hasClass('unselected')) { + let logo = $(btn).find('i') + $(btn).removeClass('unselected') + $(btn).addClass('selected') + $(logo).replaceWith('') + } else if ($(btn).hasClass('selected')) { + let logo = $(btn).find('i') + $(btn).removeClass('selected') + $(btn).addClass('unselected') + $(logo).replaceWith('') + } +} - let rarities = []; - $("#rarity-form input").each(function () { - if ($(this).is(":checked")) { - let label = $("label[for='" + $(this).attr("id") + "']")[0]["innerText"]; - rarities.push(label); - } - }); - let colors = []; - $("#color-form input").each(function () { - if ($(this).is(":checked")) { - let label = $("label[for='" + $(this).attr("id") + "']")[0]["innerText"]; - colors.push(label); - } - }); +$('.delete-from-deck-btn').each(function() { + let btn = $(this)[0] + $(btn).on('click', async function(evt) { + evt.preventDefault() + let idSplit = $(btn).attr('id').split('-') + let deckId = idSplit[0] + let cardId = idSplit[1] - let resp = await axios.get("http://127.0.0.1:5000/home/filter", { - params: { - types: `${types}`, - sets: `${sets}`, - rarities: `${rarities}`, - colors: `${colors}`, - }, - }); - console.log(resp); - filteredCards = resp.data.filtered_cards; - bookmarkedCards = resp.data.bookmarked_cards; - bookmarkedCardIDs = []; - for (let card of bookmarkedCards) { - bookmarkedCardIDs.push(card["id"]); - } - $("#cards").empty(); - for (let card of filteredCards) { - cardHTML = genereateCardHTML(card, bookmarked_cards); - $("#cards").append(cardHTML); - } -}); + await axios.post(`/cards/${cardId}/decks/${deckId}/delete`) + $(`#card-${cardId}-col`).remove() + }) +}) -function genereateCardHTML(card) { - return ` -
-
- ... - -
-
-
${card.name}
- {% for attr, value in ${card}.__dict__.items() %} {% if attr != - 'image_url' and attr != 'text' and attr != '_sa_instance_state' - and value and value != '' %} -

{{attr}}: {{value}}

- {% else %} {% endif %} {% endfor %} -
-
-
-
- {% if ${card.id} in bookmarked_card_ids %} -
- -
- {% else %} -
- -
- {% endif %} - -
-
-
-
-
-
- {% if decks | length == 0 %} - Create Deck - {% else %} - {% for deck in decks %} -
- -
- {% endfor %} - {% endif %} - -
-
-
-
-
-
`; -} +$('.delete-from-friends-btn').each(function() { + let btn = $(this)[0] + $(btn).on('click', async function(evt) { + evt.preventDefault() + let friendUsername = $(btn).attr('id') + + await axios.post(`/remove_friend/${friendUsername}`) + $(`#friend-${friendUsername}-col`).remove() + }) +}) + +$('.deck-btn').each(function() { + let btn = $(this)[0] + $(btn).on('click', async function(evt) { + evt.preventDefault() + let idSplit = $(btn).attr('id').split('-') + let cardId = idSplit[0] + let deckId = idSplit[1] + + await axios.post(`/cards/${cardId}/decks/${deckId}`) + let dropdownMenu = $(`#add-${cardId}`) + dropdownMenu.removeClass('show') + let dropdownBtn = $(`#add-${cardId}-btn`) + toggleDropdownIcon(dropdownBtn) + }) +}) \ No newline at end of file diff --git a/static/images/mtg_deck_img.jpg b/static/images/mtg_deck_img.jpg new file mode 100644 index 0000000..c1c1ccc Binary files /dev/null and b/static/images/mtg_deck_img.jpg differ diff --git a/static/images/mtg_default.png b/static/images/mtg_default.png deleted file mode 100644 index 4184ff1..0000000 Binary files a/static/images/mtg_default.png and /dev/null differ diff --git a/static/stylesheets/style.css b/static/stylesheets/style.css index 422cf74..97fb2da 100644 --- a/static/stylesheets/style.css +++ b/static/stylesheets/style.css @@ -1,9 +1,11 @@ body { height: 100vh; + width: 100vw; background-color: black; } #background-image { + z-index: -1; position: absolute; left: 50%; transform: translateX(-50%); @@ -15,7 +17,7 @@ body { } .scroll { - max-height: 88vh; + max-height: 92vh; overflow-y: auto; } @@ -33,8 +35,9 @@ ul { height: 90vh; } -#main-home { +.main-home { height: 87vh; + width: 100vw; } .nav-img { @@ -45,18 +48,14 @@ ul { max-height: 400px; } -/* input[type="checkbox"] { - display: none; +.add-to-deck-unselected:active { + transition-duration: 0.5s; } -input[type="checkbox"] + label { - background: url("images/checkbox-off.png") no-repeat; - width: 20px; - height: 20px; +.add-to-deck-selected:active { + transition-duration: 0.5s; } -input[type="checkbox"]:checked + label { - background: url("images/checkbox-on.png"); - width: 20px; - height: 20px; -} */ +.btn:focus { + box-shadow: none; +} diff --git a/templates/base.html b/templates/base.html index 66988f6..ee261d2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -20,126 +20,125 @@
{% block content %} {% endblock %} @@ -165,8 +164,7 @@ integrity="sha512-quHCp3WbBNkwLfYUMd+KwBAgpVukJu5MncuQaWXgCrfgcxCJAq/fo+oqrRKOj+UKEmyMCG3tb8RB63W+EmrOBg==" crossorigin="anonymous" > - {# + - #} diff --git a/templates/bookmarks.html b/templates/bookmarks.html index 5e0e04c..e3ac6dd 100644 --- a/templates/bookmarks.html +++ b/templates/bookmarks.html @@ -1,111 +1,27 @@ -{% extends "base.html" %} {% block content %} -
-
- {# TYPE FORM #} {{type_form.hidden_tag()}} -
-
- - -
-
-
- {% for field in type_form if field.widget.input_type != 'hidden' %} - {% if field.type == "RadioField" %} {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
-
-
- {# POWER FORM #} {{power_form.hidden_tag()}} -
-
- - -
-
-
- {% for field in power_form if field.widget.input_type != 'hidden' %} - {% if field.type == "RadioField" %} {{field}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
-
-
- - {# TOUGHNESS FORM #} {{toughness_form.hidden_tag()}} -
-
- - -
-
-
- {% for field in toughness_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} {{field}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
+{% extends "base.html" %} {% block title %}Bookmarks{% endblock %} {% +block content %} +
+
+
+

Bookmarks

+

+ These are cards that you have bookmarked for future reference. +

-
-
-
- -
-
-
- -
-

Bookmarks

-
+
{% for card in bookmarked_cards %} -
-
+
+
...
-
-
{{card.name}}
- {% for attr, value in card.__dict__.items() %} {% if attr != - 'image_url' and attr != 'text' and attr != '_sa_instance_state' - and value and value != '' %} -

{{attr}}: {{value}}

- {% else %} {% endif %} {% endfor %} +
+
+
{{card.name}}
+
+
+ + + {% for attr, value in card.__dict__.items() %} + {% if attr !='image_url' and attr != 'text' and attr != 'id' and attr != '_sa_instance_state' and value and value != '' %} + + + + + {% endif %} + {% endfor %} + +
+

{{attr.upper().replace('_', ' ')}}

+
+ {{value}} +
+
-
-
+
+
+ + + -
- -
- - -
-
-
-
-
-
- {% for deck in decks %} -
- -
- {% endfor %} -
-
-
-
+ +
+
+
+
+
+
+ {% for deck in decks %} +
+ +
+ {% endfor %} + Create Deck +
+
+
+
{% endfor %} @@ -163,4 +107,4 @@
{{card.name}}
{% endblock %} -
\ No newline at end of file +
diff --git a/templates/deck.html b/templates/deck.html index be184c3..d959c1a 100644 --- a/templates/deck.html +++ b/templates/deck.html @@ -1,162 +1,82 @@ -{% extends "base.html" %} {% block content %} -
-
- {# TYPE FORM #} {{type_form.hidden_tag()}} -
-
- - -
-
-
- {% for field in type_form if field.widget.input_type != 'hidden' %} - {% if field.type == "RadioField" %} {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
-
-
- {# POWER FORM #} {{power_form.hidden_tag()}} -
-
- - -
-
-
- {% for field in power_form if field.widget.input_type != 'hidden' %} - {% if field.type == "RadioField" %} {{field}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
-
-
- - {# TOUGHNESS FORM #} {{toughness_form.hidden_tag()}} -
-
+{% extends "base.html" %} {% block title %}{{deck.deck_name}}{% endblock %} {% +block content %} +
+
+

{{deck.deck_name}}

+

+ Owner: {{deck.username}} +

+

Type: {{deck.deck_type}}

+
+
+ {% for card in deck.cards %} +
+
+ ... - -
-
-
- {% for field in toughness_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} {{field}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
+
+
+
+
{{card.name}}
+
+ + + {% for attr, value in card.__dict__.items() %} {% if attr + !='image_url' and attr != 'text' and attr != 'id' and attr != + '_sa_instance_state' and value and value != '' %} + + + + + {% endif %} {% endfor %} + +
+

{{attr.upper().replace('_', ' ')}}

+
+ {{value}} +
-
-
-
-
- -
-
-
- -
-
- {% for card in deck.cards %} -
-
- ... +
+
+ {% if card.id in bookmarked_card_ids %} +
+ +
+ {% else %} +
+ +
+ {% endif %} -
-
-
{{card.name}}
- {% for attr, value in card.__dict__.items() %} {% if attr != - 'image_url' and attr != 'text' and attr != '_sa_instance_state' - and value and value != '' %} -

{{attr}}: {{value}}

- {% else %} {% endif %} {% endfor %} -
-
-
-
- {% if card.id in bookmarked_card_ids %} -
- -
- {% else %} -
- -
- {% endif %} -
- -
-
-
- {#
-
-
-
- {% for deck in decks %} -
- -
- {% endfor %} -
-
-
-
#}
- {% endfor %}
+ {% endfor %}
- {% endblock %} -
\ No newline at end of file +
+{% endblock %} diff --git a/templates/decks.html b/templates/decks.html index 21be1c4..f21a060 100644 --- a/templates/decks.html +++ b/templates/decks.html @@ -1,40 +1,73 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} {% block title %}Decks{% endblock %} {% block content +%} -
-
-
- {% for deck in decks %} -
+
+ {% if search == False %} +
+

{{g.user.username}}'s Decks

+

+ These are decks that {{g.user.username}} has put together from the + cards in our database. +

+
+ {% endif %} + +
+ {% for category, message in get_flashed_messages(with_categories=True) %} +

{{ message }}

+ {% endfor %} +
+
+ {% for deck in decks %} +
+
- ... + + ... +

{{deck.deck_name}}

{{deck.deck_type}}
+ {% if search == True %} +
+
+ Owner: {{deck.username}} +
+
+ {% endif %}
-
- -
- View Deck + {% if g.user and deck.username == g.user.username %} +
+ +
+ {% endif %} +
- {% endfor %}
+ {% endfor %}
diff --git a/templates/edit_user.html b/templates/edit_user.html index 7a07f81..f92c658 100644 --- a/templates/edit_user.html +++ b/templates/edit_user.html @@ -1,5 +1,5 @@ -{% extends "base.html" %} {% block title %}Login{% endblock %} {% block content -%} +{% extends "base.html" %} {% block title %}Edit Info{% endblock %} {% block +content %}
diff --git a/templates/friends.html b/templates/friends.html index f45628c..55d10c3 100644 --- a/templates/friends.html +++ b/templates/friends.html @@ -1,27 +1,41 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} {% block title %}{{user.username}}'s Friends{% +endblock %} {% block content %} -
+
+
+

{{user.username}}'s Friends

+
-
- {% for friend in friends %} -
- -

{{friend.username}}

-
Decks: {{friend.decks | length}}
- View Profile - View Decks{{ message }}

+ {% endfor %} {% for friend in friends %} +
+ +

{{friend.username}}

+
Decks: {{friend.decks | length}}
+ View Profile + View Decks +
+ -
-
+ Remove Friend + +
{% endfor %}
diff --git a/templates/home.html b/templates/home.html index 3586544..0b1bfdd 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,247 +1,24 @@ -{% extends "base.html" %} {% block content %} -
-
- {# TYPE FORM #} {{type_form.hidden_tag()}} -
-
- - -
-
-
-
- {% for field in type_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} - {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
- -
-
-
-
-
-
-
- {# POWER FORM #} {{power_form.hidden_tag()}} -
-
- - -
-
-
-
- {% for field in power_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} - {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
- -
-
-
-
-
-
-
- - {# TOUGHNESS FORM #} {{toughness_form.hidden_tag()}} -
-
- - -
-
-
-
- {% for field in toughness_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} - {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
- -
-
-
-
-
+{% extends "base.html" %} {% block title %}MTG Deck Builder{% endblock %} {% +block content %} +
+
+
+

Welcome, {{g.user.username}}!

+

+ Use the search bar above to search for cards, users, friends and decks that you've made! +

-
- {# SET FORM #} {{set_form.hidden_tag()}} -
-
- - -
-
-
-
- {% for field in set_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} - {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
- -
-
-
-
-
-
-
- {# RARITY FORM #} {{rarity_form.hidden_tag()}} -
-
- - -
-
-
-
- {% for field in rarity_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} - {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
- -
-
-
-
-
-
-
- {# COLOR FORM #} {{color_form.hidden_tag()}} -
-
- - -
-
-
-
- {% for field in color_form if field.widget.input_type != - 'hidden' %} {% if field.type == "RadioField" %} - {{field(class_="text-small")}} {% else %} - {{field(class_="form-control")}} {% endif %} {% for error in - field.errors %} - {{error}} - {% endfor %} {% endfor %} -
-
-
- -
-
-
-
-
+
+ {% for category, message in get_flashed_messages(with_categories=True) %} +

{{ message }}

+ {% endfor %}
-
-
- -
-
+
{% for card in cards %} -
+
...
-
-
{{card.name}}
- {% for attr, value in card.__dict__.items() %} {% if attr != - 'image_url' and attr != 'text' and attr != '_sa_instance_state' - and value and value != '' %} -

{{attr}}: {{value}}

- {% else %} {% endif %} {% endfor %} +
+
+
{{card.name}}
+
+
+ + + {% for attr, value in card.__dict__.items() %} + {% if attr !='image_url' and attr != 'text' and attr != 'id' and attr != '_sa_instance_state' and value and value != '' %} + + + + + {% endif %} + {% endfor %} + +
+

{{attr.upper().replace('_', ' ')}}

+
+ {{value}} +
+
{% if card.id in bookmarked_card_ids %} -
-
{% else %} -
-
{% endif %}
@@ -299,22 +94,22 @@
{{card.name}}
- {% if decks | length == 0 %} - Create Deck - {% else %} {% for deck in decks %} + + {% for deck in decks %}
-
- {% endfor %} {% endif %} + {% endfor %} + Create Deck
@@ -323,7 +118,59 @@
{{card.name}}
{% endfor %}
- {# + {% endif %}
{% endblock %} diff --git a/templates/new_deck.html b/templates/new_deck.html index 75640ef..5d16c4b 100644 --- a/templates/new_deck.html +++ b/templates/new_deck.html @@ -1,9 +1,11 @@ -{% extends "base.html" %} {% block title %}Login{% endblock %} {% block content -%} +{% extends "base.html" %} {% block title %}New Deck{% endblock %} {% block +content %}
-
+
-

Create New Deck

+
+

Create New Deck

+
{{form.hidden_tag()}} {% for field in form if field.widget.input_type != 'hidden' %} @@ -14,7 +16,11 @@

Create New Deck

{% endfor %}

{% endfor %} - +
+ +
{% for category, message in get_flashed_messages(with_categories=True) %}

{{ message }}

diff --git a/templates/register.html b/templates/register.html index 9cc6f73..f7d7f09 100644 --- a/templates/register.html +++ b/templates/register.html @@ -1,12 +1,15 @@ -{% extends "base.html" %} {% block title %}Login{% endblock %} {% block content -%} +{% extends "base.html" %} {% block title %}Register{% endblock %} {% block +content %}

Register

- {{form.hidden_tag()}} {% for field in form if field.widget.input_type != - 'hidden' %} + {% for category, message in get_flashed_messages(with_categories=True) + %} +

{{ message }}

+ {% endfor %} {{form.hidden_tag()}} {% for field in form if + field.widget.input_type != 'hidden' %}

{{field.label(class_='text-white')}} {{field(class_="form-control")}} {% for error in field.errors %} @@ -17,10 +20,6 @@

Register

- {% for category, message in get_flashed_messages(with_categories=True) - %} -

{{ message }}

- {% endfor %}

If you already have an account, diff --git a/templates/user.html b/templates/user.html index c650ae4..1396aab 100644 --- a/templates/user.html +++ b/templates/user.html @@ -1,4 +1,5 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} {% block title %}{{user.username}}{% endblock %} {% +block content %}

@@ -11,30 +12,70 @@
Decks: {{user.decks | length}}
href="/users/{{user.username}}/decks" >View Decks - {% if user.username != g.user.username and user not in g.user.friends %} Add FriendView Friends + {% if user.username != g.user.username and user not in g.user.friends %} +
+ +
{% elif user in g.user.friends %} - Remove Friend + + {% endif %}
- {% for post in user.posts %} -
-
-

{{post.title}}

-

{{post.date_time}}

-
-
+ {% if g.user.username == user.username %} +
+

Create Post

+
+ {{form.hidden_tag()}} {% for field in form if field.widget.input_type + != 'hidden' %} +

+ {{field.label(class_='text-light')}} + {{field(class_="form-control")}} {% for error in field.errors %} + {{error}} + {% endfor %} +

+ {% endfor %} +
+ +
+ {% for category, message in get_flashed_messages(with_categories=True) + %} +

{{ message }}

+ {% endfor %} +
+
+ {% endif %} {% if user.posts|length == 0 %} +

+ {{user.username}} has not made any posts yet. +

+ {% else %} {% endif %} {% for post in user.posts %} + +
+

{{post.title}}

+

{{post.username}}

+

{{post.content}}

+

+ {{post.date_time}} +

+ {% endfor %}
diff --git a/templates/users.html b/templates/users.html index 19b06f0..969946a 100644 --- a/templates/users.html +++ b/templates/users.html @@ -1,33 +1,50 @@ -{% extends "base.html" %} {% block content %} +{% extends "base.html" %} {% block title %}Users{% endblock %} {% block content +%}
-
- {% for user in users %} -
- -

{{user.username}}

-
Decks: {{user.decks | length}}
- View Profile - View Decks - {% if user.username != g.user.username and user not in g.user.friends %} -
- -
- {% elif user in g.user.friends %} -
- -
- {% endif %} -
+ {% for category, message in get_flashed_messages(with_categories=True) %} +

{{ message }}

+ {% endfor %} +
+
+ {% for user in users %} +
+ +

{{user.username}}

+
Decks: {{user.decks | length}}
+ View Profile + View Decks + {% if user.username != g.user.username and user not in g.user.friends %} +
+ +
+ {% elif user in g.user.friends %} +
+ +
+ {% endif %}
{% endfor %}
diff --git a/templates/welcome.html b/templates/welcome.html index 99284c1..8b9cddf 100644 --- a/templates/welcome.html +++ b/templates/welcome.html @@ -1,4 +1,5 @@ -{% extends 'base.html' %} {% block content %} +{% extends 'base.html' %} {% block title %}Welcome{% endblock %} {% block +content %}
diff --git a/test_bookmark_model.py b/test_bookmark_model.py index 2f63417..c7bd5cc 100644 --- a/test_bookmark_model.py +++ b/test_bookmark_model.py @@ -7,7 +7,8 @@ from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) class BookmarkModelTestCase(TestCase): @@ -15,7 +16,6 @@ class BookmarkModelTestCase(TestCase): def setUp(self): """Create test client, add sample data.""" - db.drop_all() db.create_all() @@ -35,16 +35,24 @@ def setUp(self): db.session.commit() def tearDown(self): - db.drop_all() + db.session.rollback() def test_bookmark_model(self): bookmark = Bookmark(username=self.user.username, card_id=1) db.session.add(bookmark) db.session.commit() - print(bookmark.username) - # ^^ THIS PRINTS 'usernametest', SO I HAVE NO IDEA WHY THE BELOW ASSERTION IS NOT WORKING (IT JUST PAUSES INFINITELY WITH NO OUTPUT). COULD NOT FIND A SOLUTION ONLINE - print(bookmark.card_id) - # ^^ THIS PRINTS '1', SO I HAVE NO IDEA WHY THE BELOW ASSERTION IS NOT WORKING (IT JUST PAUSES INFINITELY WITH NO OUTPUT). COULD NOT FIND A SOLUTION ONLINE + self.assertEqual(bookmark.username, 'usernametest') self.assertEqual(bookmark.card_id, 1) + + def test_bookmark_serialize(self): + bookmark = Bookmark(username=self.user.username, + card_id=1) + db.session.add(bookmark) + db.session.commit() + + self.assertEqual(bookmark.serialize(), + {'id': bookmark.id, + 'username': bookmark.username, + 'card_id': bookmark.card_id}) diff --git a/test_card_model.py b/test_card_model.py index 99a9282..282ee71 100644 --- a/test_card_model.py +++ b/test_card_model.py @@ -3,11 +3,12 @@ from unittest import TestCase import requests -from models import db, User, Card, Bookmark, bcrypt +from models import db, User, Card, Bookmark from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) class CardModelTestCase(TestCase): @@ -15,7 +16,8 @@ class CardModelTestCase(TestCase): def setUp(self): """Create test client, add sample data.""" - + Bookmark.query.delete() + Card.query.delete() db.create_all() self.client = app.test_client() @@ -24,19 +26,32 @@ def setUp(self): 'key': "$2a$10$TNyqKQQQSzVjgGXY87waZuBIKAS78.NkY2o.H004TfBU.eISv.Pt6", 'page': 1 }).json() - print(self.resp) + self.cards = self.resp['cards'] - print(self.cards) + Card.create_all_cards(self.cards) - print('Cards Created') - db.session.commit() - self.card = Card.query.filter(Card.name == 'Abundance').first() + db.session.commit() def tearDown(self): - db.drop_all() + db.session.rollback() def test_card_model(self): - # <-- THIS PRINTS 'Abundance', SO I HAVE NO IDEA WHY THE BELOW ASSERTION IS NOT WORKING (IT JUST PAUSES INFINITELY WITH NO OUTPUT). COULD NOT FIND A SOLUTION ONLINE - print(self.card.name) - self.assertEqual(self.card.name, 'Abundance') + """Test that basic card model works""" + card = Card.query.filter(Card.name == 'Abundance').first() + self.assertEqual(card.name, 'Abundance') + print(card.name) + + def test_create_all_cards(self): + print('TESTING') + for page in range(2, 4): + print(f'Page: {page}') + """Test that create_all_cards method works correctly""" + resp = requests.get('http://api.magicthegathering.io/v1/cards', { + 'key': "$2a$10$TNyqKQQQSzVjgGXY87waZuBIKAS78.NkY2o.H004TfBU.eISv.Pt6", + 'page': page + }).json() + cards = resp['cards'] + Card.create_all_cards(cards) + print(len(Card.query.all())) + self.assertEqual(len(Card.query.all()), 300) diff --git a/test_deck_model.py b/test_deck_model.py index a76ee25..5ed1aed 100644 --- a/test_deck_model.py +++ b/test_deck_model.py @@ -7,7 +7,8 @@ from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) class DeckModelTestCase(TestCase): @@ -15,6 +16,9 @@ class DeckModelTestCase(TestCase): def setUp(self): """Create test client, add sample data.""" + Deck.query.delete() + Bookmark.query.delete() + User.query.delete() db.create_all() @@ -25,13 +29,17 @@ def setUp(self): self.deck = Deck(deck_name='Sample Deck', deck_type='Standard', username=self.user.username) + self.user.decks.append(self.deck) db.session.commit() def tearDown(self): - db.drop_all() + db.session.rollback() def test_deck_model(self): + """Test that basic deck model works""" + print(self.deck.deck_name) self.assertEqual(self.deck.deck_name, 'Sample Deck') self.assertEqual(self.deck.deck_type, 'Standard') self.assertEqual(self.deck.username, 'username') + self.assertIn(self.deck, self.user.decks) diff --git a/test_deck_routes.py b/test_deck_routes.py index 5b593fe..fec1211 100644 --- a/test_deck_routes.py +++ b/test_deck_routes.py @@ -8,7 +8,8 @@ from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) class DeckRoutesTestCase(TestCase): @@ -31,10 +32,11 @@ def setUp(self): db.session.commit() print(self.deck1) - def tearDown(self): - db.session.rollback() + # def tearDown(self): + # db.session.rollback() def test_view_decks(self): + """Test that route for viewing your own decks works""" with self.client as c: with c.session_transaction() as sess: sess[CURR_USER_KEY] = self.user1.username @@ -48,17 +50,26 @@ def test_view_decks(self): '
Standard
', str(resp.data)) def test_show_deck(self): + """Test that route for viewing contents of your own deck works""" with self.client as c: with c.session_transaction() as sess: sess[CURR_USER_KEY] = self.user1.username - resp = c.get(f'/decks/{self.deck1.id}') + resp = c.get(f'/decks/1') self.assertEqual(resp.status_code, 200) - # def test_delete_deck(self): - # def test_create_deck(self): + + def test_delete_deck(self): + """Test that route for deleting a deck works""" + + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + resp = c.post(f'/decks/1/delete', follow_redirects=True) + self.assertEqual(resp.status_code, 200) def test_add_to_deck(self): + """Test that route for adding a card to your deck works""" with self.client as c: with c.session_transaction() as sess: sess[CURR_USER_KEY] = self.user1.username @@ -69,9 +80,33 @@ def test_add_to_deck(self): db.session.add(new_card) db.session.commit() print(card) - resp = c.post(f'/cards/{new_card.id}/decks/{self.deck1.id}', + resp = c.post(f'/cards/{new_card.id}/decks/1', follow_redirects=True) self.assertEqual(resp.status_code, 200) - # def test_delete_from_deck(self): - # def test_show_users_deck(self): + + def test_delete_from_deck(self): + """Test that route for removing a card from your deck works""" + + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + + card = mtgsdk.Card.where(name='Abundance').all()[0] + new_card = Card(name=card.name, card_type=card.type, + colors=card.colors, rarity=card.rarity, set_name=card.set_name) + db.session.add(new_card) + db.session.commit() + c.post(f'/cards/1/decks/1', + follow_redirects=True) + resp = c.post(f'/cards/1/decks/1/delete', follow_redirects=True) + self.assertEqual(resp.status_code, 200) + + def test_show_users_deck(self): + """Test that route for showing a user's deck works""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + + resp = c.get(f'/users/{self.user1.username}/decks') + self.assertEqual(resp.status_code, 200) diff --git a/test_friend_model.py b/test_friend_model.py index 2c42fcd..0d1de1e 100644 --- a/test_friend_model.py +++ b/test_friend_model.py @@ -7,7 +7,8 @@ from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) class FriendModelTestCase(TestCase): @@ -33,5 +34,6 @@ def tearDown(self): db.drop_all() def test_friend_model(self): + """Test that basic friend model works""" self.assertEqual(self.friendship.user1_username, 'username_1') self.assertEqual(self.friendship.user2_username, 'username_2') diff --git a/test_friend_routes.py b/test_friend_routes.py new file mode 100644 index 0000000..7316eae --- /dev/null +++ b/test_friend_routes.py @@ -0,0 +1,62 @@ +from app import app, CURR_USER_KEY +import os +from unittest import TestCase +from users import do_logout +from models import db, User, Friendship + + +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) + + +class FriendRoutesTestCase(TestCase): + """Test views for messages.""" + + def setUp(self): + """Create test client, add sample data.""" + db.drop_all() + db.create_all() + + self.client = app.test_client() + + self.user1 = User.signup(email='email1@gmail.com', password='user1password', username='username_1', + image_url=None) + self.user2 = User.signup(email='email2@gmail.com', password='user2password', username='username_2', + image_url=None) + db.session.commit() + + def tearDown(self): + db.session.rollback() + + def test_add_friend(self): + """Test that route for adding a friend works""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + resp = c.post('/add_friend/username_2', follow_redirects=True) + + self.assertEqual(resp.status_code, 200) + # self.assertIn(self.user2, self.user1.friends) + self.assertEqual(len(Friendship.query.all()), 1) + self.assertEqual(len(Friendship.query.filter( + Friendship.user1_username == 'username_2' or Friendship.user2_username == 'username_2').all()), 1) + + def test_show_friends(self): + """Test that route for showing your friends works""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + c.post('/add_friend/username_1', follow_redirects=True) + resp = c.get('/users/username_1/friends', follow_redirects=True) + + self.assertEqual(resp.status_code, 200) + + def test_remove_friend(self): + """Test that route for removing a friend works""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + c.post('/add_friend/username_2') + resp = c.post('/remove_friend/username_2', follow_redirects=True) + + self.assertEqual(resp.status_code, 200) diff --git a/test_home_routes.py b/test_home_routes.py new file mode 100644 index 0000000..21b69c0 --- /dev/null +++ b/test_home_routes.py @@ -0,0 +1,172 @@ +from app import app, CURR_USER_KEY +import os +from unittest import TestCase +import requests +from home import determine_page, determine_index_range, render_homepage +from models import db, User, Card, Deck + +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) + + +class HomeRoutesTestCase(TestCase): + + def setUp(self): + """Create test client, add sample data.""" + + db.drop_all() + db.create_all() + + self.client = app.test_client() + + self.user1 = User.signup(email='email1@gmail.com', password='user1password', username='username_1', + image_url=None) + self.user2 = User.signup(email='email2@gmail.com', password='user2password', username='username_2', + image_url=None) + + self.user1.friends.append(self.user2) + + self.deck = Deck(deck_name='Test Deck', + deck_type='Standard', username='username_1') + + self.user1.decks.append(self.deck) + + self.card = Card(name='Test Name', text='Sample Text', card_type='Creature', power='1', + toughness='2', colors='Blue', rarity='Common', set_name='Tenth Edition') + db.session.add(self.card) + + db.session.commit() + + def tearDown(self): + db.session.rollback() + + def test_determine_page(self): + """Test that determine_page function works""" + req_args = {'page': 1} + page = determine_page(req_args) + self.assertEquals(page, 1) + + def test_determine_index_range(self): + """Test that determine_index_range function works""" + index_range = determine_index_range(2) + self.assertEqual(index_range, range(101, 201)) + + def test_welcome(self): + """Test that root route takes you to welcome page when no user is logged in""" + with self.client as c: + c.get('/logout') + resp = c.get('/') + + self.assertEqual(resp.status_code, 200) + self.assertIn( + 'Welcome to MTG Deck Builder!', str(resp.data)) + + def test_show_homepage(self): + """Test that root route takes you to home page when user is logged in""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = self.user1.username + resp = c.get('/', follow_redirects=True) + + self.assertEqual(resp.status_code, 200) + self.assertIn('Logout', str(resp.data)) + self.assertIn('Show Info', str(resp.data)) + self.assertIn('Add to Deck', str(resp.data)) + + def test_search_card_exact(self): + """Test that the search route for a card gives the proper result (using exact term)""" + with self.client as c: + resp = c.get('/home/search?category=card&term=Test+Name') + + self.assertEqual(resp.status_code, 200) + self.assertIn('Show Info', str(resp.data)) + self.assertIn('Add to Deck', str(resp.data)) + self.assertIn('
', str(resp.data)) + self.assertIn( + '
Test Name
', str(resp.data)) + self.assertIn( + 'Test Name', str(resp.data)) + + def test_search_card_substring(self): + """Test that the search route for a card gives the proper result (using substring/casefold)""" + with self.client as c: + resp = c.get('/home/search?category=card&term=teSt') + + self.assertEqual(resp.status_code, 200) + self.assertIn('Show Info', str(resp.data)) + self.assertIn('Add to Deck', str(resp.data)) + self.assertIn('
', str(resp.data)) + self.assertIn( + '
Test Name
', str(resp.data)) + self.assertIn('Test Name', str(resp.data)) + + def test_search_user_exact(self): + """Test that the search route for a user gives the proper result (using exact term)""" + with self.client as c: + resp = c.get('/home/search?category=user&term=username_1') + + self.assertEqual(resp.status_code, 200) + self.assertIn('View Profile', str(resp.data)) + self.assertIn('View Decks', str(resp.data)) + self.assertIn( + '

username_1

', str(resp.data)) + + def test_search_user_substring(self): + """Test that the search route for a user gives the proper result (using substring/casefold)""" + with self.client as c: + resp = c.get('/home/search?category=user&term=Usern') + + self.assertEqual(resp.status_code, 200) + self.assertIn('View Profile', str(resp.data)) + self.assertIn('View Decks', str(resp.data)) + self.assertIn( + '

username_1

', str(resp.data)) + + def test_search_friend_exact(self): + """Test that the search route for a friend gives the proper result (using exact term)""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = 'username_1' + resp = c.get('/home/search?category=friend&term=username_2') + + self.assertEqual(resp.status_code, 200) + self.assertIn('View Profile', str(resp.data)) + self.assertIn('View Decks', str(resp.data)) + self.assertIn( + '

username_2

', str(resp.data)) + + def test_search_friend_substring(self): + """Test that the search route for a friend gives the proper result (using substring/casefold)""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = 'username_1' + resp = c.get('/home/search?category=friend&term=Usern') + + self.assertEqual(resp.status_code, 200) + self.assertIn('View Profile', str(resp.data)) + self.assertIn('View Decks', str(resp.data)) + self.assertIn( + '

username_2

', str(resp.data)) + + def test_search_deck_exact(self): + """Test that the search route for a deck gives the proper result (using exact term)""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = 'username_1' + resp = c.get('/home/search?category=deck&term=Test+Deck') + self.assertEqual(resp.status_code, 200) + self.assertIn('View Deck', str(resp.data)) + self.assertIn( + '

Test Deck

', str(resp.data)) + + def test_search_deck_substring(self): + """Test that the search route for a deck gives the proper result (using substring/casefold)""" + with self.client as c: + with c.session_transaction() as sess: + sess[CURR_USER_KEY] = 'username_1' + resp = c.get('/home/search?category=deck&term=tEsT+D') + + self.assertEqual(resp.status_code, 200) + self.assertIn('View Deck', str(resp.data)) + self.assertIn( + '

Test Deck

', str(resp.data)) diff --git a/test_post_model.py b/test_post_model.py new file mode 100644 index 0000000..1c0f58f --- /dev/null +++ b/test_post_model.py @@ -0,0 +1,42 @@ +from app import app +import os +from unittest import TestCase +import requests + +from models import db, User, Post, Deck, Bookmark + +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) + + +class PostModelTestCase(TestCase): + """Test Post model""" + + def setUp(self): + """Create test client, add sample data.""" + Deck.query.delete() + Bookmark.query.delete() + User.query.delete() + + db.create_all() + + self.client = app.test_client() + + self.user = User.signup(email='email@gmail.com', password='userpassword', username='username', + image_url=None) + + self.post = Post(username="username", title="Test Title", + content="This is test content for a post!") + self.user.posts.append(self.post) + + db.session.commit() + + def tearDown(self): + db.session.rollback() + + def test_post_model(self): + """Test that basic post model works""" + self.assertEqual(self.post.username, 'username') + self.assertEqual(self.post.title, 'Test Title') + self.assertEqual(self.post.content, 'This is test content for a post!') + self.assertIn(self.post, self.user.posts) diff --git a/test_user_model.py b/test_user_model.py index 020b456..5ff9fcc 100644 --- a/test_user_model.py +++ b/test_user_model.py @@ -6,7 +6,8 @@ from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) def test(username, password, email, image_url): @@ -33,30 +34,17 @@ def setUp(self): db.session.commit() - # def tearDown(self): - # db.session.rollback() + def tearDown(self): + db.session.rollback() def test_user_model(self): """Does basic model work?""" - u = User( - email="test@test.com", - username="testuser", - password="HASHED_PASSWORD" - ) - - db.session.add(u) - db.session.commit() - # User should have no messages & no followers - self.assertEqual(len(u.decks), 0) - self.assertEqual(len(u.bookmarked_cards), 0) - self.assertEqual(len(u.friends), 0) - self.assertEqual(len(u.posts), 0) - - # ********** - # Does the repr method work as expected? - # ********** + self.assertEqual(len(self.user1.decks), 0) + self.assertEqual(len(self.user1.bookmarked_cards), 0) + self.assertEqual(len(self.user1.friends), 0) + self.assertEqual(len(self.user1.posts), 0) def test_user_model_repr(self): """Does basic model repr work?""" @@ -65,6 +53,7 @@ def test_user_model_repr(self): f'{self.user1}', f'') def test_user_signup(self): + """Test that signup class method works""" self.assertEqual(self.user1.username, 'username_1') self.assertTrue(bcrypt.check_password_hash( self.user1.password, 'user1password')) @@ -73,10 +62,12 @@ def test_user_signup(self): "/static/images/default_prof_pic.png") def test_user_authenticate(self): - username = self.user1.username - good_password = 'user1password' - bad_password = 'user2password' + """Test that authenticate class method works""" self.assertEqual(User.authenticate( - username=username, password=good_password), self.user1) + username=self.user1.username, password='user1password'), self.user1) + self.assertFalse(User.authenticate( + username=self.user1.username, password='user2password')) + self.assertFalse(User.authenticate( + username=self.user2.username, password='user1password')) self.assertFalse(User.authenticate( - username=username, password=bad_password)) + username='someotherusername', password='someotherpassword')) diff --git a/test_user_routes.py b/test_user_routes.py index 4b8dba4..4b3724e 100644 --- a/test_user_routes.py +++ b/test_user_routes.py @@ -1,19 +1,15 @@ from app import app, CURR_USER_KEY import os from unittest import TestCase -from users import check_confirmed_pwd - +from users import check_confirmed_pwd, handle_register_form_errors +from flask import redirect +from forms import NewPostForm, RegisterForm from models import db, User, Bookmark, bcrypt from sqlalchemy.exc import IntegrityError from psycopg2.errors import UniqueViolation -os.environ['DATABASE_URL'] = "postgresql:///mtg_db_test" - - -def test(username, password, email, image_url): - user = User.signup(email=email, password=password, username=username, - image_url=image_url) - db.session.commit() +app.config['SQLALCHEMY_DATABASE_URI'] = ( + os.environ.get('DATABASE_URL', 'postgres:///mtg_db_test')) class UserRoutesTestCase(TestCase): @@ -38,6 +34,7 @@ def tearDown(self): db.session.rollback() def test_register(self): + """Test that register route works""" with self.client as c: resp = c.get('/register') @@ -48,6 +45,7 @@ def test_register(self): '', str(resp.data)) def test_login(self): + """Test that login route works""" with self.client as c: resp = c.get('/login') @@ -58,6 +56,7 @@ def test_login(self): '', str(resp.data)) def test_logout(self): + """Test that logout route works""" with self.client as c: with c.session_transaction() as sess: sess[CURR_USER_KEY] = self.user1.username @@ -71,6 +70,7 @@ def test_logout(self): '', str(resp.data)) def test_user_profile(self): + """Test that route for viewing a user's profile works""" with self.client as c: with c.session_transaction() as sess: sess[CURR_USER_KEY] = self.user1.username @@ -82,14 +82,82 @@ def test_user_profile(self): self.assertIn('
Decks: 0
', str(resp.data)) def test_edit_profile(self): + """Test that route for editing your own profile works""" with self.client as c: with c.session_transaction() as sess: sess[CURR_USER_KEY] = self.user1.username - resp = c.get(f'/users/{{self.user1.username}}/edit') + resp = c.get(f'/users/{self.user1.username}/edit') self.assertEqual(resp.status_code, 200) self.assertIn('Email', str(resp.data)) self.assertIn('Password', str(resp.data)) self.assertIn('Confirm Password', str(resp.data)) self.assertIn('Profile Picture', str(resp.data)) + + def test_register_form_valid(self): + """Test that function for handling register form errors works (with no errors, this executes the complete_register() function)""" + with self.client as c: + with app.test_request_context(): + form_data = { + 'email': 'email@gmail.com', + 'username': 'username123', + 'password': 'password321', + 'confirmed_password': 'password321', + 'image_url': None + } + form = RegisterForm(data=form_data) + handle_register_form_errors(form) + self.assertTrue(User.query.get('username123')) + + def test_register_form_taken_email(self): + """Test that function for handling register form errors redicrects to /register if email taken""" + with self.client as c: + with app.test_request_context(): + form_data = { + 'email': 'email1@gmail.com', + 'username': 'username123', + 'password': 'password321', + 'confirmed_password': 'password321', + 'image_url': None + } + form = RegisterForm(data=form_data) + resp = handle_register_form_errors(form) + self.assertFalse(User.query.get('username123')) + self.assertEqual( + '/register', resp.location) + + def test_register_form_taken_username(self): + """Test that function for handling register form errors redicrects to /register if username taken""" + with self.client as c: + with app.test_request_context(): + form_data = { + 'email': 'email@gmail.com', + 'username': 'username_1', + 'password': 'password321', + 'confirmed_password': 'password321', + 'image_url': None + } + form = RegisterForm(data=form_data) + resp = handle_register_form_errors(form) + self.assertEqual(len(User.query.filter( + User.email == 'email@gmail.com').all()), 0) + self.assertEqual( + '/register', resp.location) + + def test_register_form_nonmatching_pwd(self): + """Test that function for handling register form errors redicrects to /register if passwords don't match""" + with self.client as c: + with app.test_request_context(): + form_data = { + 'email': 'email@gmail.com', + 'username': 'username123', + 'password': 'password321', + 'confirmed_password': 'password3210', + 'image_url': None + } + form = RegisterForm(data=form_data) + resp = handle_register_form_errors(form) + self.assertFalse(User.query.get('username123')) + self.assertEqual( + '/register', resp.location) diff --git a/users.py b/users.py index e10d101..622848a 100644 --- a/users.py +++ b/users.py @@ -5,20 +5,17 @@ import mtgsdk import flask_paginate -# from mtgsdk import Type from app import g from flask import Flask, Blueprint, session, request, render_template, redirect, flash from flask_debugtoolbar import DebugToolbarExtension -from models import db, connect_db, User, Friendship, Message, Card, Bookmark, Deck, CardDeck, Post -from forms import LoginForm, RegisterForm, TypeForm, PowerForm, ToughnessForm, DeckForm, EditUserForm +from models import db, connect_db, User, Friendship, Card, Bookmark, Deck, CardDeck, Post, bcrypt +from forms import LoginForm, RegisterForm, DeckForm, EditUserForm, NewPostForm users_blueprint = Blueprint('users_blueprint', __name__, static_folder='static', template_folder='templates') CURR_USER_KEY = 'curr-user' -TYPES = mtgsdk.Type.all() - def do_logout(): """Logout user.""" @@ -42,19 +39,36 @@ def register(): form = RegisterForm() if form.validate_on_submit(): - check_confirmed_pwd(form.password.data, form.confirmed_password.data) - image_url = form.image_url.data or "/static/images/default_prof_pic.png" - user = User.signup(username=form.username.data, password=form.password.data, - email=form.email.data, image_url=image_url) - if user: - db.session.commit() - session[CURR_USER_KEY] = form.username.data - return redirect('/home') - flash('Username or password is incorrect', 'danger') - return redirect('/login') + return handle_register_form_errors(form) return render_template('register.html', form=form) +def handle_register_form_errors(form): + """Determines & handles errors found in register user form. If no errors found, creates new user.""" + if User.query.get(form.username.data): + flash( + f'The username "{form.username.data}" is already taken', 'danger') + return redirect('/register') + if len(User.query.filter(User.email == form.email.data).all()) > 0: + flash('That email address is already taken', 'danger') + return redirect('/register') + if not check_confirmed_pwd(form.password.data, form.confirmed_password.data): + flash('Passwords must match - please try again.', 'danger') + return redirect('/register') + return complete_register(form) + + +def complete_register(form): + """Creates a new user from the form data and logs that user in""" + image_url = form.image_url.data or "/static/images/default_prof_pic.png" + if User.signup(username=form.username.data, password=form.password.data, + email=form.email.data, image_url=image_url): + db.session.commit() + session[CURR_USER_KEY] = form.username.data + return redirect('/home') + return redirect('/login') + + @users_blueprint.route('/login', methods=['GET', 'POST']) def login(): """ @@ -81,47 +95,71 @@ def login(): @users_blueprint.route('/logout') def logout(): - do_logout() + """Logs out a user and redirets them to the login page""" + if g.user: + do_logout() return redirect('/login') def check_confirmed_pwd(pwd, confirmed_pwd): + """Checks that the confirmed password matches upon registering""" if pwd != confirmed_pwd: - flash('Passwords must match - please try again.', 'danger') - return redirect('/register') + return False + return True -@users_blueprint.route('/users/') +@users_blueprint.route('/users/', methods=['GET', 'POST']) def user_profile(username): - user = User.query.get(username) - return render_template('user.html', user=user) + """ + GET: Route for viewing a user's profile + POST: Create a new Post instance and put it on your page + """ + if g.user: + form = NewPostForm() + if g.user.username == username: + if form.validate_on_submit(): + post = Post(username=username, title=form.title.data, + content=form.content.data) + db.session.add(post) + db.session.commit() + return redirect(f'/users/{username}') + user = User.query.get(username) + return render_template('user.html', user=user, form=form) + return redirect('/login') @users_blueprint.route('/users//edit', methods=['GET', 'POST']) def edit_profile(username): + """ + GET: Route for editting your profile + POST: Update the user's data + """ if g.user: form = EditUserForm() user = User.query.get(g.user.username) if form.validate_on_submit(): user.email = form.email.data - if form.password.data == form.confirmed_password.data: - user.password = form.password.data + user.image_url = form.image_url.data or "/static/images/default_prof_pic.png" + if check_confirmed_pwd(form.password.data, form.confirmed_password.data): + user.password = bcrypt.generate_password_hash( + form.password.data).decode('UTF-8') else: flash('Passwords do not match - please try again.', 'danger') return redirect(f'/users/{user.username}/edit') - user.image_url = form.image_url.data or "/static/images/default_prof_pic.png" - db.session.add(user) db.session.commit() - return redirect('/home') - form.email.data = user.email - form.password.data = user.password - form.confirmed_password.data = user.password - form.image_url.data = user.image_url - + populate_edit_profile_fields(form, user) return render_template('edit_user.html', form=form) return render_template('/login') + + +def populate_edit_profile_fields(form, user): + """Populates edit user form fields""" + form.email.data = user.email + form.password.data = user.password + form.confirmed_password.data = user.password + form.image_url.data = user.image_url