diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c908c..ebcab7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,19 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.0.7] - 2024-05-05 ### Added 1. Models subpackage: - User class: * get_dogs() method + * __str__() method that returns name attribute + * status attribute + - Dog class: + * get_age() method to calculate age of dog from birth_date attribute + * __str__() method that returns name attribute + - Visit class: + * __str__() method that returns date_time attribute + * __repr__() method that returns to_dict() method 2. App factory now in app_factory.py 3. Development and testing now have different configurations. - development_config.py - tests/testing_config.py 4. Makefile: - Default help target +5. Views submodule: + - users.py: + * user_dogs_panel() view for users dashboard to config dogs +6. Jinja templates: + - Users my dogs panel for users dashboard +7. Tests: + - test_dog_model.py to test Dog model ### Changed @@ -25,6 +40,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - User class: * get_bookings() method renamed to get_visits() * get_bookings_history() method renamed to get_visit_history() + - Dog class: + * age attribute renamed to birth_date + * status attribute now linked to User status + * __repr__() method now return to_dict() method + - Dog Submodule: + * BreedSize values are now specific + - Visit Submodule: + * VisitType values are now specific 2. Tests package: - conftest.py: * Updated to use new app_factory.py @@ -32,24 +55,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - test_user_model.py: * Updated to use new app_factory.py * Variables updated to reflect methods in User model -3. Views package: + * test number of dogs user owns +3. Views submodel: - Users dashboard route: * Update User model object usage to reflect renamed methods * Renamed variable user_dogs to user_pets + * user_pets template variable renamed to user_dogs + - User API endpoints: + * user_pets() view renamed to user_dogs() + * user_dogs() endpoint renamed to /users//dogs 4. Makefile: - Target commands are no longer echoed when run. +5. Jinja templates: + - Login and Registration: + * Initial form work + - Users Dashboard: + * Page title now includes "Account Dashboard" + * pets_overview.html renamed to dogs_overview.html + * dogs_overview.html started using Bootstrap for styling + * visits_overview.html started using Bootstrap for styling + ### Removed 1. Models subpackage: - User class: * removed redundent import from get_visit_history() method +2. Nix shell: + - Exported environment variables +3. Tests: + - test_user_model.py: + * Unused imports ### Fixed 1. Fixed typo in CHANGELOG.md 2. Makefile: - Comments used by help target now use correct symbol (##) +3. Models subpackage: + - Dog class: + * to_dict() method should now return dict representation of Dog object + - User class: + * get_visit_history() method renamed to get_visit_overview() + * to_dict() method should now return dict representation of User object + - Visit class: + * to_dict() method should now return dict representation of Visit object +4. Jinja templates: + - Users Dashboard: + * Page header changed to "Account Dashboard" + * Update dog overview include +5. App factory: + - DB dog test data updated to use birth_date attribute + - DB test data for the dog Rufus changed to reflect actual breed_size ## [0.0.6] - 2024-04-28 diff --git a/accounts/models/dog/__init__.py b/accounts/models/dog/__init__.py index 3cdefd0..3faca76 100644 --- a/accounts/models/dog/__init__.py +++ b/accounts/models/dog/__init__.py @@ -2,6 +2,7 @@ from datetime import datetime from sqlalchemy import Enum +from sqlalchemy.ext.hybrid import hybrid_property # Import db from accounts.models import db @@ -13,23 +14,44 @@ class Dog(db.Model): __tablename__ = 'dogs' id = db.Column(db.Integer, primary_key=True, autoincrement=True) + owner = db.relationship('User', back_populates='dogs') + owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) name = db.Column(db.String, nullable=False) breed_size = db.Column(Enum(BreedSize), default=BreedSize.MEDIUM) - age = db.Column(db.DateTime, default=datetime(1970, 1, 1, 0, 0, 0)) - owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) - owner = db.relationship('User', back_populates='dogs') + birth_date = db.Column(db.DateTime, default=datetime(1970, 1, 1)) visits = db.relationship('Visit', back_populates='dogs') - status = db.Column(Enum(Status), default=Status.ACTIVE) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) + def __str__(self): + return self.name + def __repr__(self): - return f'{self.name}' + return self.to_dict() + + @hybrid_property + def status(self): + return self.owner.status + + def get_age(self): + current_date = datetime.now() + age = (current_date.year - self.birth_date.year + - ((current_date.month, current_date.day) < (self.birth_date.month, self.birth_date.day))) + + return age def to_dict(self): return { + 'id': self.id, + 'owner': self.owner, + 'owner_id': self.owner_id, 'name': self.name, - 'breed': self.breed, - 'owner': self.owner + 'breed_size': self.breed_size, + 'birth_date': self.birth_date, + 'age': self.get_age(), + 'visits': self.visits, + 'status': self.status, + 'created_at': self.created_at, + 'updated_at': self.updated_at } diff --git a/accounts/models/dog/breed_size.py b/accounts/models/dog/breed_size.py index adb56e4..1cfe2d8 100644 --- a/accounts/models/dog/breed_size.py +++ b/accounts/models/dog/breed_size.py @@ -2,7 +2,8 @@ from enum import Enum from enum import auto class BreedSize(Enum): - SMALL = auto() - MEDIUM = auto() - LARGE = auto() + SMALL = 'Small' + MEDIUM = 'Medium' + LARGE = 'Large' + XLARGE = 'X-large' diff --git a/accounts/models/user.py b/accounts/models/user.py index 7a0f9af..2f2f2fb 100644 --- a/accounts/models/user.py +++ b/accounts/models/user.py @@ -1,8 +1,12 @@ from datetime import datetime +from sqlalchemy import Enum + # Import db from accounts.models import db +from accounts.models.status import Status + class User(db.Model): __tablename__ = 'users' @@ -14,11 +18,15 @@ class User(db.Model): password = db.Column(db.String, nullable=False) dogs = db.relationship('Dog', back_populates='owner') visits = db.relationship('Visit', back_populates='owner') + status = db.Column(Enum(Status), default=Status.ACTIVE) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) + def __str__(self): + return self.name + def __repr__(self): - return f'{self.username}' + return self.to_dict() # Get all dogs currently set def get_dogs(self): @@ -29,15 +37,23 @@ class User(db.Model): return [visit for visit in self.visits] # Retrieve past booked visits - def get_visit_history(self): + def get_visit_overview(self): current_time = datetime.now() return [visit for visit in self.visits if visit.date_time < current_time] def to_dict(self): return { + 'name': self.name, 'username': self.username, 'email': self.email, + 'address': self.address, 'password': self.password, + 'dogs': self.get_dogs(), + 'visits': self.get_visits(), + 'status': self.status, + 'created_at': self.created_at, + 'updated_at': self.updated_at + } diff --git a/accounts/models/visit/__init__.py b/accounts/models/visit/__init__.py index 4179cc2..49bd515 100644 --- a/accounts/models/visit/__init__.py +++ b/accounts/models/visit/__init__.py @@ -17,12 +17,18 @@ class Visit(db.Model): date_time = db.Column(db.DateTime, nullable=False) owner_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) owner = db.relationship('User', back_populates='visits') - visit_type = db.Column(Enum(VisitType), default=VisitType.MEDIUM_MINUTE_WALK) + visit_type = db.Column(Enum(VisitType), default=VisitType.MEDIUM_WALK) dog_id = db.Column(db.Integer, db.ForeignKey('dogs.id'), nullable=False) dogs = db.relationship('Dog', back_populates='visits') created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) + def __str__(self): + return self.date_time + + def __repr__(self): + return self.to_dict() + def get_date(self): year = self.date_time.year month = self.date_time.month @@ -42,9 +48,14 @@ class Visit(db.Model): def to_dict(self): return { - 'ID': self.id, - 'Date-Time': self.date_time, - 'Owner-ID': self.owner_id, - 'Dog-ID': self.dog_id + 'id': self.id, + 'date_time': self.date_time, + 'owner_id': self.owner_id, + 'owner': self.owner, + 'visit_type': self.visit_type, + 'dog_id': self.dog_id, + 'dogs': self.dogs, + 'created_at': self.created_at, + 'updated_at': self.update_at } diff --git a/accounts/models/visit/visit_type.py b/accounts/models/visit/visit_type.py index a373941..e3ea808 100644 --- a/accounts/models/visit/visit_type.py +++ b/accounts/models/visit/visit_type.py @@ -2,8 +2,8 @@ from enum import Enum from enum import auto class VisitType(Enum): - SMALL_MINUTE_WALK = auto() - MEDIUM_MINUTE_WALK = auto() - LARGE_MINUTE_WALK = auto() - HOUSE_SITTING = auto() + SMALL_WALK = '15-minute walk' + MEDIUM_WALK = '30-minute walk' + LARGE_WALK = '45-minute walk' + HOUSE_SITTING = 'House sitting' diff --git a/accounts/templates/login.html b/accounts/templates/login.html index af44a18..30a649b 100644 --- a/accounts/templates/login.html +++ b/accounts/templates/login.html @@ -1,10 +1,21 @@ {% extends "base.html" %} -{% set title %}Account Login{% endset %} +{% set title %}Login{% endset %} {% block content %} -Login here ---------------- -Soon to come ;) +

Login

+ +
+ +
+
+ + +
+
+ + +
+
{% endblock %} diff --git a/accounts/templates/registration.html b/accounts/templates/registration.html index b0d1e12..5e4c241 100644 --- a/accounts/templates/registration.html +++ b/accounts/templates/registration.html @@ -3,8 +3,42 @@ {% set title %}Account Registration{% endset %} {% block content %} -Register for an account ------------------------ -Soon to come ;) +

Register for an account

+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
{% endblock %} diff --git a/accounts/templates/users/dashboard/base.html b/accounts/templates/users/dashboard/base.html index 5f22aad..b851291 100644 --- a/accounts/templates/users/dashboard/base.html +++ b/accounts/templates/users/dashboard/base.html @@ -1,19 +1,16 @@ {% extends 'base.html' %} -{% set title %}{{ user.username }} - Dashboard{% endset %} +{% set title %}{{ user.username }} - Account Dashboard{% endset %} {% block content %} -

Account Overview

+

Account Dashboard


- -

Pets

-{% include 'users/dashboard/pets_overview.html' %} +{% include 'users/dashboard/dogs_overview.html' %}
-

Booking History

{% include 'users/dashboard/visits_overview.html' %} {% endblock %} diff --git a/accounts/templates/users/dashboard/dogs_overview.html b/accounts/templates/users/dashboard/dogs_overview.html new file mode 100644 index 0000000..42f176f --- /dev/null +++ b/accounts/templates/users/dashboard/dogs_overview.html @@ -0,0 +1,27 @@ +
+ + + + + + + + + + + + + + + {% for dog in user_dogs %} + + + + + + + {% endfor %} + +
Dogs overview
NameBreed sizeBirth dateAge
{{ dog.name }}{{ dog.breed_size.value }}{{ dog.birth_date.strftime('%Y-%m-%d') }}{{ dog.get_age() }}
+
+ diff --git a/accounts/templates/users/dashboard/pets_overview.html b/accounts/templates/users/dashboard/pets_overview.html deleted file mode 100644 index 61e4dbe..0000000 --- a/accounts/templates/users/dashboard/pets_overview.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - -
NameBreed
- diff --git a/accounts/templates/users/dashboard/visits_overview.html b/accounts/templates/users/dashboard/visits_overview.html index 07f954a..3993a83 100644 --- a/accounts/templates/users/dashboard/visits_overview.html +++ b/accounts/templates/users/dashboard/visits_overview.html @@ -1,18 +1,27 @@ - - - - - - - +
+
TypeDateTimeDog
+ + + + + + + + + + + - {% for visit in user.visits %} - - - - - - - {% endfor %} -
Visits overview
TypeDateTimePets
{{ visit.type }}{{ visit.get_date() }}{{ visit.get_time() }}{{ visit.dog }}
+ + {% for visit in user_book_history %} + + {{ visit.visit_type.value }} + {{ visit.get_date() }} + {{ visit.get_time() }} + {{ visit.dogs }} + + {% endfor %} + + + diff --git a/accounts/templates/users/my_dogs/base.html b/accounts/templates/users/my_dogs/base.html new file mode 100644 index 0000000..41570e7 --- /dev/null +++ b/accounts/templates/users/my_dogs/base.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% set title %}{{ user.username }} - My Dogs{% endset %} + +{% block content %} +

My Dogs

+ +
+ +{% include 'users/my_dogs/dogs_panel.html' %} +{% endblock %} + diff --git a/accounts/templates/users/my_dogs/dogs_panel.html b/accounts/templates/users/my_dogs/dogs_panel.html new file mode 100644 index 0000000..9ed9594 --- /dev/null +++ b/accounts/templates/users/my_dogs/dogs_panel.html @@ -0,0 +1,27 @@ +
+ + + + + + + + + + + + + + + {% for dog in user_dogs %} + + + + + + + {% endfor %} + +
My dogs
NameBreed sizeBirth dateAge
{{ dog.name }}{{ dog.breed_size.value }}{{ dog.birth_date.strftime('%Y-%m-%d') }}{{ dog.get_age() }}
+
+ diff --git a/accounts/views/users.py b/accounts/views/users.py index 8368909..9ffdec2 100644 --- a/accounts/views/users.py +++ b/accounts/views/users.py @@ -18,20 +18,23 @@ def user_dashboard(username): # Retrieve user data user = db.session.execute(db.select(User).filter_by(username=username)).scalar_one() - return render_template('users/dashboard/base.html', user=user, user_pets=user.get_dogs(), user_book_history=user.get_visits()) + return render_template('users/dashboard/base.html', user=user, user_dogs=user.get_dogs(), user_book_history=user.get_visits()) + +@accounts.route('/users//my_dogs', methods=['GET']) +def user_dogs_panel(username): + # Retrieve user data + user = db.session.execute(db.select(User).filter_by(username=username)).scalar_one() + + return render_template('users/my_dogs/base.html', user=user, user_dogs=user.get_dogs()) @accounts.route('/users/', methods=['POST']) def user(username): user = db.session.execute(db.select(User).filter_by(username=username)).scalar_one() return jsonify(user.to_dict()), 201 -@accounts.route('/users//pets', methods=['POST']) -def user_pets(username): +@accounts.route('/users//dogs', methods=['POST']) +def user_dogs(username): user = db.session.execute(db.select(User).filter_by(username=username)).scalar_one() - dogs = list() - for dog in user.dogs: - dogs.append(dog.name) - - return jsonify(dogs), 201 + return jsonify(user.get_dogs()), 201 diff --git a/app_factory.py b/app_factory.py index c618926..6e0d5cc 100755 --- a/app_factory.py +++ b/app_factory.py @@ -56,13 +56,17 @@ def add_test_dogs(db): lassey = Dog(name='Lassey', breed_size=BreedSize.LARGE, + birth_date=datetime(2010, 10, 4), owner_id=man_man.id) rufus = Dog(name='Rufus', - breed_size=BreedSize.SMALL, + breed_size=BreedSize.MEDIUM, + birth_date=datetime(2020, 4, 20), owner_id=man_man.id) air_bud = Dog(name='Air Bud', + breed_size=BreedSize.LARGE, + birth_date=datetime(2018, 7, 16), owner_id=real_person.id) db.session.add(lassey) diff --git a/shell.nix b/shell.nix index 9d0000d..13f5aa7 100644 --- a/shell.nix +++ b/shell.nix @@ -25,9 +25,5 @@ pkgs.mkShell { sqlite-utils # utilities ])) ]; - - shellHook = '' - export FLASK_APP=tests.testing_app - ''; } diff --git a/tests/test_dog_model.py b/tests/test_dog_model.py new file mode 100644 index 0000000..a53d656 --- /dev/null +++ b/tests/test_dog_model.py @@ -0,0 +1,30 @@ +# Import libraries +import pytest + +# Import db +from accounts.models import db + +# Import db models +from accounts.models.dog import Dog + +def test_user_model(app, db): + lassey = db.session.execute(db.select(Dog).where(Dog.name == 'Lassey')).scalar_one() + rufus = db.session.execute(db.select(Dog).where(Dog.name == 'Rufus')).scalar_one() + air_bud = db.session.execute(db.select(Dog).where(Dog.name == 'Air Bud')).scalar_one() + + # Assert something + assert lassey.id == 1 + assert lassey.name == 'Lassey' + assert lassey.breed_size.value == 'Large' + assert lassey.owner_id == 1 + + assert rufus.id == 2 + assert rufus.name == 'Rufus' + assert rufus.breed_size.value == 'Medium' + assert rufus.owner_id == 1 + + assert air_bud.id == 3 + assert air_bud.name == 'Air Bud' + assert air_bud.breed_size.value == 'Large' + assert air_bud.owner_id == 2 + diff --git a/tests/test_user_model.py b/tests/test_user_model.py index ddcbf0c..47e985f 100644 --- a/tests/test_user_model.py +++ b/tests/test_user_model.py @@ -4,18 +4,17 @@ import pytest # Import db from accounts.models import db -# Import db models -from accounts.models.dog import Dog -from accounts.models.dog.breed_size import BreedSize +# Import user db model from accounts.models.user import User -from accounts.models.visit.visit_type import VisitType def test_user_model(app, db): man_man = db.session.execute(db.select(User).where(User.name == 'Man Man')).scalar_one() man_man_visits = man_man.get_visits() + man_man_dogs = man_man.get_dogs() real_person = db.session.execute(db.select(User).where(User.name == 'Real Person')).scalar_one() real_person_visits = real_person.get_visits() + real_person_dogs = real_person.get_dogs() # Assert something assert man_man.id == 1 @@ -25,6 +24,7 @@ def test_user_model(app, db): assert man_man.address == '123 Home Ln. City, AA 11223' assert man_man.password == 'manword' assert len(man_man_visits) == 2 + assert len(man_man_dogs) == 2 assert real_person.id == 2 assert real_person.name == 'Real Person' @@ -33,4 +33,5 @@ def test_user_model(app, db): assert real_person.address == '113 Park St. City, AA 13433' assert real_person.password == 'realpassword' assert len(real_person_visits) == 1 + assert len(real_person_dogs) == 1