diff --git a/README.md b/README.md index cc852f0..4d17f1a 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,9 @@ $ python app.py ``` Visit [http://localhost:5000/](http://localhost:5000/) in your browser to see the results. + +### Running Tests + +``` +python test_models.py +``` diff --git a/app.py b/app.py index 4347b45..c604601 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///desserts.db' db = SQLAlchemy(app) +app.secret_key = '\xa5MY\xb4\xc7\x03\x04\xa5I+C\x86\x1e\xae\x04\xb2-\xe4|\x06\x1e\x7f\x1f4' if __name__ == "__main__": @@ -14,7 +15,6 @@ # the app, so we import them. We could do it earlier, but there's # a risk that we may run into circular dependencies, so we do it at the # last minute here. - from views import * app.run(debug=True) diff --git a/backup.csv b/backup.csv new file mode 100644 index 0000000..e69de29 diff --git a/backup.py b/backup.py new file mode 100644 index 0000000..94cf89c --- /dev/null +++ b/backup.py @@ -0,0 +1,27 @@ +from models import * + + +def save_data(): + + with open('backup.csv', 'w') as f: # Open file 'backup.csv' for writing + + for dessert in Dessert.query.all(): + # Create a comma separated line + line = "{},{},{}\n".format(dessert.name, dessert.price, + dessert.calories) + # Write it to the file + f.write(line) + +def load_data(): + + with open('backup.csv') as f: + for line in f: + name, price, calories = line.split(',') + d = Dessert(name, price, calories) + db.session.add(d) + db.session.commit() + + +if __name__=="__main__": + # save_data() + load_data() \ No newline at end of file diff --git a/desserts.db b/desserts.db new file mode 100644 index 0000000..9dde879 Binary files /dev/null and b/desserts.db differ diff --git a/models.py b/models.py index e40d530..d2ea5c4 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,17 @@ from app import db +from flask import session + +class Menu(db.Model): + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100)) + + def __init__(self, name): + self.name = name + +# DESSERTS class Dessert(db.Model): # See http://flask-sqlalchemy.pocoo.org/2.0/models/#simple-example # for details on the column types. @@ -13,39 +24,176 @@ class Dessert(db.Model): price = db.Column(db.Float) calories = db.Column(db.Integer) - def __init__(self, name, price, calories): + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship("User", backref="desserts") + + def __init__(self, name, price, calories, user_id): self.name = name self.price = price self.calories = calories + self.user_id = user_id def calories_per_dollar(self): if self.calories: return self.calories / self.price +def create_dessert(new_name, new_price, new_calories, user_id): + # Create a dessert with the provided input. + # We need every piece of input to be provided. -class Menu(db.Model): + # Can you think of other ways to write this following check? + if new_name is None or new_price is None or new_calories is None or user_id is None: + raise Exception("Need name, price, calories, and user!") - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100)) + # They can also be empty strings if submitted from a form + if new_name == '' or new_price == '' or new_calories == '': + raise Exception("Need name, price and calories!") - def __init__(self, name): - self.name = name + if int(new_calories) > 10000: + raise Exception("That can't be right. Check the calories again.") + if int(new_price) > 100: + raise Exception("That can't be right. No dessert is worth that much.") -def create_dessert(new_name, new_price, new_calories): - # Create a dessert with the provided input. - # At first, we will trust the user. + if Dessert.query.filter_by(name=new_name).first(): + raise Exception("There's already one of those. Pick another.") # This line maps to line 16 above (the Dessert.__init__ method) - dessert = Dessert(new_name, new_price, new_calories) + dessert = Dessert(new_name, new_price, new_calories, user_id) # Actually add this dessert to the database db.session.add(dessert) # Save all pending changes to the database - db.session.commit() + try: + db.session.commit() + return dessert + except: + # If something went wrong, explicitly roll back the database + db.session.rollback() + +def update_dessert(dessert_name, dessert_price, dessert_cals, id): + # Create a dessert with the provided input. + # We need every piece of input to be provided. + dessert = Dessert.query.get(id) + + # Can you think of other ways to write this following check? + if dessert_name is None or dessert_price is None or dessert_cals is None: + raise Exception("Need name, price and calories!") + + # They can also be empty strings if submitted from a form + if dessert_name == '' or dessert_price == '' or dessert_cals == '': + raise Exception("Need name, price and calories!") + + if int(dessert_cals) > 10000: + raise Exception("That can't be right. Check the calories again.") + + if float(dessert_price) > 100: + raise Exception("That can't be right. No dessert is worth that much.") + + # This is where the dessert gets updated + dessert.name = dessert_name + dessert.price = dessert_price + dessert.calories = dessert_cals + print 'dessert updated' - return dessert + # Save all pending changes to the database + try: + db.session.commit() + + return dessert + except: + # If something went wrong, explicitly roll back the database + db.session.rollback() + +def delete_dessert(id): + dessert = Dessert.query.get(id) + if dessert: + # We store the name before deleting it, because we can't access it + # afterwards. + dessert_name = dessert.name + db.session.delete(dessert) + + try: + db.session.commit() + return "Dessert {} deleted".format(dessert_name) + except: + # If something went wrong, explicitly roll back the database + db.session.rollback() + return "Something went wrong" + else: + return "Dessert not found" + + +# USERS +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100)) + password = db.Column(db.String(100)) + email = db.Column(db.String(250)) + name = db.Column(db.String(100)) + avatar = db.Column(db.String(250)) + + def __init__(self, username, password, email, name, avatar): + self.username = username + self.password = password + self.email = email + self.name = name + self.avatar = avatar + +def log_in(username, password): + if username is None or password is None: + raise Exception("Please include your username and password.") + + # They can also be empty strings if submitted from a form + if username == '' or password == '': + raise Exception("Please include your username and password.") + + try: + user = get_user_by_username(username) + session["user_id"] = user.id + return user + except: + raise Exception("You don't have an account.") + +def list_users(): + return User.query.all() + +def get_user(id): + return User.query.get(id) + +def get_user_by_username(username): + return User.query.filter_by(username=username).first() + +def create_user(username, email, password, realname, avatar): + user = User(username, email, password, realname, avatar) + db.session.add(user) + db.session.commit() + return user + +def update_user(id, username=None, email=None, password=None, name=None, + avatar=None): + # This one is harder with the object syntax actually! So we changed the + # function definition. + user = User.query.get(id) + + if username: + user.username = username + + if email: + user.email = email + + if password: + user.password = password + + if name: + user.name = name + + if avatar: + user.avatar = avatar + + db.session.commit() + return user if __name__ == "__main__": diff --git a/templates/account.html b/templates/account.html new file mode 100644 index 0000000..c632246 --- /dev/null +++ b/templates/account.html @@ -0,0 +1,19 @@ +{% include 'header.html' %} + + +
+

{{ user.name }}'s Account Details

+ + + + + See your desserts +
+ \ No newline at end of file diff --git a/templates/add.html b/templates/add.html index e2d174a..814e7cc 100644 --- a/templates/add.html +++ b/templates/add.html @@ -1,27 +1,44 @@ -

Add Dessert

- -{% if dessert %} - -

{{dessert.name}} successfully created! Add another below.

-

Back to Dessert List

- -{% endif %} - - - -
- - - - - - - - - - - - - - -
\ No newline at end of file +{% include 'header.html' %} + + +
+

Add Dessert

+ {% if dessert %} + + + + {% endif %} + + {% if error %} + + {% endif %} + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+ \ No newline at end of file diff --git a/templates/create-account.html b/templates/create-account.html new file mode 100644 index 0000000..7871928 --- /dev/null +++ b/templates/create-account.html @@ -0,0 +1,11 @@ +

Confirm Your Account Details

+ + + +Yup, Let's Log In \ No newline at end of file diff --git a/templates/details.html b/templates/details.html new file mode 100644 index 0000000..c7262a0 --- /dev/null +++ b/templates/details.html @@ -0,0 +1,33 @@ +{% include 'header.html' %} + + + +
+

Dessert Details

+ +

{{ dessert.name }}

+ +

Price: ${{ dessert.price }}

+ +

Calories: {{ dessert.calories }}

+ +

Calories per dollar: {{ dessert.calories_per_dollar() }}

+ +

Created by: {{ dessert.user.name }}

+ + Delete {{ dessert.name }} + Edit {{ dessert.name }} +

 

+

Back to Dessert List

+
+ + + + + \ No newline at end of file diff --git a/templates/edit.html b/templates/edit.html new file mode 100644 index 0000000..8548b21 --- /dev/null +++ b/templates/edit.html @@ -0,0 +1,42 @@ +{% include 'header.html' %} + + +
+

Edit Dessert

+ + + {% if error %} + + {% endif %} + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+ \ No newline at end of file diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..55af09a --- /dev/null +++ b/templates/header.html @@ -0,0 +1,19 @@ + + + Desserts + + + + + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html index 8220939..14c2f55 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,9 +1,29 @@ -

Dessert Menu

+{% include 'header.html' %} - + +
+

Log In

-Add Item \ No newline at end of file + {% if error %} + + {% endif %} + +

Please log in.

+ +
+ + + + + + + +
+ +

Don't have an account?

+ Sign Up +
+ \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..18e0c34 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,22 @@ +{% include 'header.html' %} + + +
+

Log In

+ +

Please log in.

+ +
+ + + + + + + +
+ +

Don't have an account?

+ Sign Up +
+ \ No newline at end of file diff --git a/templates/menu.html b/templates/menu.html new file mode 100644 index 0000000..869f4f8 --- /dev/null +++ b/templates/menu.html @@ -0,0 +1,17 @@ +{% include 'header.html' %} + + +
+ Your account + +

{{ user.name }}'s Dessert Menu

+ + + + Add Item +
+ \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..836fecf --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,28 @@ +{% include 'header.html' %} + + +
+

Sign Up

+ +

Please create an account.

+ +
+ + + + + + + + + + + + + + + + +
+
+ \ No newline at end of file diff --git a/test_models.py b/test_models.py new file mode 100644 index 0000000..92914c1 --- /dev/null +++ b/test_models.py @@ -0,0 +1,114 @@ +import traceback + +from models import db, create_dessert, delete_dessert + + +def check_test(func): + """ This is a decorator that simply prints out whether the function + it calls succeeded or not. You don't need to edit this. + """ + def func_wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + print ":) {} passed".format(func.__name__) + except AssertionError: + traceback.print_exc() + print ":( {} failed".format(func.__name__) + return func_wrapper + + +# ## Testing validation for the create_dessert method. + +@check_test +def test_create_dessert_works(): + test_name = "Test Dessert" + test_price = 10 + test_calories = 200 + + dessert = create_dessert(test_name, test_price, test_calories) + + assert dessert is not None + + # Delete this dessert now we are done + db.session.delete(dessert) + db.session.commit() + + +@check_test +def test_create_dessert_wrong_types(): + # Test we can't create a dessert with the wrong type of input + + test_name = 4 + test_price = "Cake" + test_calories = "None" + + # Initialize the dessert variable so we can check it later. + dessert = None + + # "Try" to create the dessert, and do nothing (pass) if we get an error. + # After this next block of code, dessert should still be None. + + try: + dessert = create_dessert(test_name, test_price, test_calories) + except Exception: + pass + + # Check dessert is still not created. + assert dessert is None + + +@check_test +def test_create_dessert_missing_data(): + # Test that if we pass 'None' in, we fail. + dessert = None + + try: + dessert = create_dessert(None, None, None) + except Exception: + # You could use e.message in here to check that the error message + # is correct if you like. + pass + + # Check dessert is still not created. + assert dessert is None + + # Also try with empty strings + try: + dessert = create_dessert('', '', '') + except Exception: + pass + + # Check dessert is still not created. + assert dessert is None + + # Are there other values we should test for? What about a sensible + # range for each item? + + +@check_test +def test_delete_dessert(): + dessert = create_dessert('test', 0, 0) + + message = delete_dessert(dessert.id) + + assert message == 'Dessert test deleted' + + +@check_test +def test_delete_nonexistent_dessert(): + random_id = 394812018 # I'm pretty sure a dessert by this ID doesn't exist + + message = delete_dessert(random_id) + + assert message == 'Dessert not found' + + +if __name__ == "__main__": + + # Run every method in this file which starts with test_. + + for item in dir(): + # Loop through all the defined items we know about (functions, etc). + # If the name starts with test_, assume it's a test and run it! + if item.startswith('test_'): + globals()[item]() diff --git a/views.py b/views.py index 4e42ae5..247b851 100644 --- a/views.py +++ b/views.py @@ -1,26 +1,29 @@ -from flask import render_template, request +from flask import render_template, request, session -from models import Dessert, create_dessert +from models import * from app import app @app.route('/') def index(): + return render_template('index.html') + # desserts = Dessert.query.all() + # return render_template('index.html', desserts=desserts) - desserts = Dessert.query.all() - - return render_template('index.html', desserts=desserts) - +@app.route('/menu') +def menu(): + user_id = session.get("user_id") + if user_id: + user = User.query.get(user_id) + desserts = Dessert.query.filter_by(user_id=user.id).all() + return render_template('menu.html', desserts=desserts, user=user) @app.route('/add', methods=['GET', 'POST']) def add(): - if request.method == 'GET': return render_template('add.html') - # Because we 'returned' for a 'GET', if we get to this next bit, we must # have received a POST - # Get the incoming data from the request.form dictionary. # The values on the right, inside get(), correspond to the 'name' # values in the HTML form that was submitted. @@ -29,5 +32,90 @@ def add(): dessert_price = request.form.get('price_field') dessert_cals = request.form.get('cals_field') - dessert = create_dessert(dessert_name, dessert_price, dessert_cals) - return render_template('add.html', dessert=dessert) + user_id = session.get("user_id") + user = User.query.get(user_id) + dessert_user = user.id + + # Now we are checking the input in create_dessert, we need to handle + # the Exception that might happen here. + + # Wrap the thing we're trying to do in a 'try' block: + try: + dessert = create_dessert(dessert_name, dessert_price, dessert_cals, dessert_user) + return render_template('add.html', dessert=dessert) + except Exception as e: + # Oh no, something went wrong! + # We can access the error message via e.message: + return render_template('add.html', error=e.message) + +@app.route('/edit/', methods=['GET', 'POST']) +def edit(id): + dessert = Dessert.query.get(id) + + if request.method == 'GET': + return render_template('edit.html', dessert=dessert) + + dessert_name = request.form.get('name_field') + dessert_price = request.form.get('price_field') + dessert_cals = request.form.get('cals_field') + + # Wrap the thing we're trying to do in a 'try' block: + try: + dessert = update_dessert(dessert_name, dessert_price, dessert_cals, id) + return render_template('details.html', dessert=dessert, id=dessert.id) + except Exception as e: + # Oh no, something went wrong! + # We can access the error message via e.message: + return render_template('edit.html', error=e.message, dessert=dessert) + +@app.route('/desserts/') +def view_dessert(id): + # We could define this inside its own function but it's simple enough + # that we don't really need to. + dessert = Dessert.query.get(id) + return render_template('details.html', dessert=dessert) + +@app.route('/delete/') +def delete(id): + message = delete_dessert(id) + return menu() # Look at the URL bar when you do this. What happens? + + +# USER MANAGEMENT +@app.route("/signup") +def signup(): + return render_template("signup.html") + +@app.route("/create-account", methods=['POST']) +def create_account(): + username = request.form.get('username') + password = request.form.get('password') + name = request.form.get('name') + email = request.form.get('email') + avatar = request.form.get('avatar') + user = create_user(username, email, password, name, avatar) + return render_template("account.html", user=user) + +@app.route("/login") +def login(): + return render_template("login.html") + +@app.route("/user", methods=['POST']) +def user_account(): + username = request.form.get('name') + password = request.form.get('password') + + try: + user = log_in(username, password) + return render_template("account.html", user=user) + except Exception as e: + # Oh no, something went wrong! + # We can access the error message via e.message: + return render_template('index.html', error=e.message) + +@app.route("/user/") +def view_user(id): + user = User.query.get(id) + return render_template("account.html", user=user) + +