├── .github └── workflows │ └── django.yml ├── .gitignore ├── LICENSE ├── README.md ├── config ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── foodtracker ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── requirements.txt ├── setup.cfg ├── static ├── css │ ├── bootstrap.min.css │ └── styles.css ├── images │ ├── FoodList.png │ ├── FoodLog.png │ ├── favicon.ico │ └── no_image.png └── js │ ├── foodDetails.js │ ├── foodLog.js │ ├── foods.js │ └── userProfile.js └── templates ├── base.html ├── categories.html ├── food.html ├── food_add.html ├── food_category.html ├── food_log.html ├── food_log_delete.html ├── index.html ├── login.html ├── register.html ├── user_profile.html └── weight_log_delete.html /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | pull_request: 8 | branches: [main] 9 | 10 | env: 11 | SECRET_KEY: yoursecretkey 12 | DEBUG: True 13 | DATABASE_NAME: postgres 14 | DATABASE_USER: postgres 15 | DATABASE_PASS: postgres 16 | DATABASE_HOST: localhost 17 | 18 | jobs: 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | strategy: 23 | max-parallel: 4 24 | matrix: 25 | python-version: [3.8, 3.9, '3.10'] 26 | 27 | services: 28 | postgres: 29 | image: postgres:latest 30 | env: 31 | POSTGRES_USER: postgres 32 | POSTGRES_PASSWORD: postgres 33 | POSTGRES_DB: food 34 | ports: 35 | - 5432:5432 36 | # needed because the postgres container does not provide a healthcheck 37 | options: >- 38 | --health-cmd pg_isready 39 | --health-interval 10s 40 | --health-timeout 5s 41 | --health-retries 5 42 | 43 | steps: 44 | - name: Checkout source Git branch 45 | uses: actions/checkout@v4 46 | 47 | - name: Set up Python ${{ matrix.python-version }} 48 | uses: actions/setup-python@v4 49 | with: 50 | python-version: ${{ matrix.python-version }} 51 | 52 | - name: Install Dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install -r requirements.txt 56 | 57 | - name: Run migrations 58 | run: | 59 | python manage.py makemigrations 60 | python manage.py migrate 61 | 62 | - name: Lint with flake8 63 | run: | 64 | flake8 65 | 66 | - name: Run Tests 67 | run: | 68 | python manage.py test 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .env 3 | .DS_Store 4 | db.sqlite3 5 | venv/ 6 | */migrations/* 7 | !*/migrations/__init__.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bob's Programming Academy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Food Tracker 2 | 3 | This is a food-tracking application built using **Django 4**, **HTML 5**, **CSS 3**, and **Bootstrap 5** with a **Bootswatch** theme. The app uses a **PostgreSQL** database to store data. Charts were built using **Chart.js 2**. 4 | 5 | ![plot](https://github.com/BobsProgrammingAcademy/Food-Tracker-Django-Bootstrap/blob/main/static/images/FoodList.png?raw=true) 6 | 7 | ![plot](https://github.com/BobsProgrammingAcademy/Food-Tracker-Django-Bootstrap/blob/main/static/images/FoodLog.png?raw=true) 8 | 9 | 10 | ## Table of Contents 11 | 12 | - [Prerequisites](#prerequisites) 13 | - [Installation](#installation) 14 | - [Running the application](#run-the-application) 15 | - [Running the tests](#run-the-tests) 16 | - [Adding data to the application](#add-data-to-the-application) 17 | - [Copyright and License](#copyright-and-license) 18 | 19 | 20 | ## Prerequisites 21 | 22 | Install the following prerequisites: 23 | 24 | 1. [Python 3.8-3.11](https://www.python.org/downloads/) 25 |
This project uses **Django v4.2.4**. For Django to work, you must install a correct version of Python on your machine. More information [here](https://django.readthedocs.io/en/stable/faq/install.html). 26 | 2. [PostgreSQL](https://www.postgresql.org/download/) 27 | 3. [Visual Studio Code](https://code.visualstudio.com/download) 28 | 29 | 30 | ## Installation 31 | 32 | ### 1. Create a virtual environment 33 | 34 | From the **root** directory, run: 35 | 36 | ```bash 37 | python -m venv venv 38 | ``` 39 | 40 | ### 2. Activate the virtual environment 41 | 42 | From the **root** directory, run: 43 | 44 | On macOS: 45 | 46 | ```bash 47 | source venv/bin/activate 48 | ``` 49 | 50 | On Windows: 51 | 52 | ```bash 53 | venv\scripts\activate 54 | ``` 55 | 56 | ### 3. Install required dependencies 57 | 58 | From the **root** directory, run: 59 | 60 | ```bash 61 | pip install -r requirements.txt 62 | ``` 63 | 64 | ### 4. Set up a PostgreSQL database 65 | 66 | With **PostgreSQL** up and running, in a new Terminal window, run: 67 | 68 | ```bash 69 | dropdb --if-exists food 70 | ``` 71 | 72 | Start **psql**, which is a terminal-based front-end to PostgreSQL, by running the command: 73 | 74 | ```bash 75 | psql postgres 76 | ``` 77 | 78 | Create a new PostgreSQL database: 79 | 80 | ```sql 81 | CREATE DATABASE food; 82 | ``` 83 | 84 | Create a new database admin user: 85 | 86 | ```sql 87 | CREATE USER yourusername WITH SUPERUSER PASSWORD 'yourpassword'; 88 | ``` 89 | 90 | To quit **psql**, run: 91 | 92 | ```bash 93 | \q 94 | ``` 95 | 96 | ### 5. Set up environment variables 97 | 98 | From the **root** directory, run: 99 | 100 | ```bash 101 | touch .env 102 | ``` 103 | 104 | The **touch** command will create the **.env** file in the **root** directory. This command works on Mac and Linux but not on Windows. If you are a Windows user, instead of using the command line, you can create the **.env** file manually by navigating in Visual Studio Code to the Explorer and selecting the option **New File**. 105 | 106 | Next, declare environment variables in the **.env** file. Make sure you don't use quotation marks around the strings. 107 | 108 | ```bash 109 | SECRET_KEY=yoursecretkey 110 | DEBUG=True 111 | DATABASE_NAME=food 112 | DATABASE_USER=yourusername 113 | DATABASE_PASS=yourpassword 114 | DATABASE_HOST=localhost 115 | ``` 116 | 117 | ### 6. Run migrations 118 | 119 | From the **root** directory, run: 120 | 121 | ```bash 122 | python manage.py makemigrations 123 | ``` 124 | 125 | ```bash 126 | python manage.py migrate 127 | ``` 128 | 129 | ### 7. Create an admin user to access the Django Admin interface 130 | 131 | From the **root** directory, run: 132 | 133 | ```bash 134 | python manage.py createsuperuser 135 | ``` 136 | 137 | When prompted, enter a username, email, and password. 138 | 139 | ## Run the application 140 | 141 | From the **root** directory, run: 142 | 143 | ```bash 144 | python manage.py runserver 145 | ``` 146 | 147 | ## Run the tests 148 | 149 | From the **root** directory, run: 150 | 151 | ```bash 152 | python manage.py test --pattern="tests.py" 153 | 154 | ``` 155 | 156 | ## View the application 157 | 158 | Go to http://127.0.0.1:8000/ to view the application. 159 | 160 | ## Add data to the application 161 | 162 | Add data through Django Admin. 163 | 164 | Go to http://127.0.0.1:8000/admin to access the Django Admin interface and sign in using the admin credentials. 165 | 166 | ## Copyright and License 167 | 168 | Copyright © 2022 Bob's Programming Academy. Code released under the MIT license. 169 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/config/__init__.py -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from decouple import config 3 | 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | SECRET_KEY = config('SECRET_KEY') 8 | 9 | DEBUG = config('DEBUG') 10 | 11 | ALLOWED_HOSTS = [ 12 | 'localhost', 13 | '127.0.0.1', 14 | ] 15 | 16 | 17 | # Application definition 18 | 19 | INSTALLED_APPS = [ 20 | 'django.contrib.admin', 21 | 'django.contrib.auth', 22 | 'django.contrib.contenttypes', 23 | 'django.contrib.sessions', 24 | 'django.contrib.messages', 25 | 'django.contrib.staticfiles', 26 | 27 | # Local 28 | 'foodtracker', 29 | ] 30 | 31 | MIDDLEWARE = [ 32 | 'django.middleware.security.SecurityMiddleware', 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.middleware.common.CommonMiddleware', 35 | 'django.middleware.csrf.CsrfViewMiddleware', 36 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 37 | 'django.contrib.messages.middleware.MessageMiddleware', 38 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 39 | ] 40 | 41 | ROOT_URLCONF = 'config.urls' 42 | 43 | TEMPLATES = [ 44 | { 45 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 46 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 47 | 'APP_DIRS': True, 48 | 'OPTIONS': { 49 | 'context_processors': [ 50 | 'django.template.context_processors.debug', 51 | 'django.template.context_processors.request', 52 | 'django.contrib.auth.context_processors.auth', 53 | 'django.contrib.messages.context_processors.messages', 54 | ], 55 | }, 56 | }, 57 | ] 58 | 59 | WSGI_APPLICATION = 'config.wsgi.application' 60 | 61 | 62 | # Database 63 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 64 | 65 | DATABASES = { 66 | 'default': { 67 | 'ENGINE': 'django.db.backends.postgresql', 68 | 'NAME': config('DATABASE_NAME'), 69 | 'USER': config('DATABASE_USER'), 70 | 'PASSWORD': config('DATABASE_PASS'), 71 | 'HOST': config('DATABASE_HOST'), 72 | 'PORT': '', # leave blank so the default port is selected 73 | } 74 | } 75 | 76 | AUTH_USER_MODEL = 'foodtracker.User' 77 | 78 | 79 | # Password validation 80 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 81 | 82 | AUTH_PASSWORD_VALIDATORS = [ 83 | { 84 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 85 | }, 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 94 | }, 95 | ] 96 | 97 | 98 | # Internationalization 99 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 100 | 101 | LANGUAGE_CODE = 'en-us' 102 | 103 | TIME_ZONE = 'UTC' 104 | 105 | USE_I18N = True 106 | 107 | USE_L10N = True 108 | 109 | USE_TZ = True 110 | 111 | 112 | # Static files (CSS, JavaScript, Images) 113 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 114 | 115 | STATIC_URL = '/static/' 116 | 117 | # Location where Django collects all static files 118 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 119 | 120 | # Location where we will store our static files 121 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] 122 | 123 | MEDIA_URL = '/media/' 124 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 125 | 126 | 127 | # Default primary key field type 128 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 129 | 130 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 131 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import path, include 5 | 6 | 7 | urlpatterns = [ 8 | path('admin/', admin.site.urls), 9 | path('', include('foodtracker.urls')) 10 | ] 11 | 12 | if settings.DEBUG: 13 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 14 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 15 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /foodtracker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/foodtracker/__init__.py -------------------------------------------------------------------------------- /foodtracker/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import User, Food, FoodCategory, FoodLog, Image, Weight 4 | 5 | 6 | admin.site.register(User) 7 | admin.site.register(Food) 8 | admin.site.register(FoodCategory) 9 | admin.site.register(FoodLog) 10 | admin.site.register(Image) 11 | admin.site.register(Weight) 12 | -------------------------------------------------------------------------------- /foodtracker/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FoodtrackerConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'foodtracker' 7 | -------------------------------------------------------------------------------- /foodtracker/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Food, Image 4 | 5 | 6 | class FoodForm(forms.ModelForm): 7 | ''' 8 | A ModelForm class for adding a new food item 9 | ''' 10 | class Meta: 11 | model = Food 12 | fields = ['food_name', 'quantity', 'calories', 'fat', 'carbohydrates', 'protein', 'category'] 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(FoodForm, self).__init__(*args, **kwargs) 16 | for visible in self.visible_fields(): 17 | visible.field.widget.attrs['class'] = 'form-control' 18 | 19 | 20 | class ImageForm(forms.ModelForm): 21 | ''' 22 | A ModelForm class for adding an image to the food item 23 | ''' 24 | class Meta: 25 | model = Image 26 | fields = ['image'] 27 | 28 | def __init__(self, *args, **kwargs): 29 | super(ImageForm, self).__init__(*args, **kwargs) 30 | self.visible_fields()[0].field.widget.attrs['class'] = 'form-control' 31 | -------------------------------------------------------------------------------- /foodtracker/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/foodtracker/migrations/__init__.py -------------------------------------------------------------------------------- /foodtracker/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | 4 | 5 | class User(AbstractUser): 6 | def __str__(self): 7 | return f'{self.username}' 8 | 9 | 10 | class FoodCategory(models.Model): 11 | category_name = models.CharField(max_length=50) 12 | 13 | class Meta: 14 | verbose_name = 'Food Category' 15 | verbose_name_plural = 'Food Categories' 16 | 17 | def __str__(self): 18 | return f'{self.category_name}' 19 | 20 | @property 21 | def count_food_by_category(self): 22 | return Food.objects.filter(category=self).count() 23 | 24 | 25 | class Food(models.Model): 26 | food_name = models.CharField(max_length=200) 27 | quantity = models.DecimalField(max_digits=7, decimal_places=2, default=100.00) 28 | calories = models.IntegerField(default=0) 29 | fat = models.DecimalField(max_digits=7, decimal_places=2) 30 | carbohydrates = models.DecimalField(max_digits=7, decimal_places=2) 31 | protein = models.DecimalField(max_digits=7, decimal_places=2) 32 | category = models.ForeignKey(FoodCategory, on_delete=models.CASCADE, related_name='food_category') 33 | 34 | def __str__(self): 35 | return f'{self.food_name} - category: {self.category}' 36 | 37 | 38 | class Image(models.Model): 39 | food = models.ForeignKey(Food, on_delete=models.CASCADE, related_name='get_images') 40 | image = models.ImageField(upload_to='images/') 41 | 42 | def __str__(self): 43 | return f'{self.image}' 44 | 45 | 46 | class FoodLog(models.Model): 47 | user = models.ForeignKey(User, on_delete=models.CASCADE) 48 | food_consumed = models.ForeignKey(Food, on_delete=models.CASCADE) 49 | 50 | class Meta: 51 | verbose_name = 'Food Log' 52 | verbose_name_plural = 'Food Log' 53 | 54 | def __str__(self): 55 | return f'{self.user.username} - {self.food_consumed.food_name}' 56 | 57 | 58 | class Weight(models.Model): 59 | user = models.ForeignKey(User, on_delete=models.CASCADE) 60 | weight = models.DecimalField(max_digits=7, decimal_places=2) 61 | entry_date = models.DateField() 62 | 63 | class Meta: 64 | verbose_name = 'Weight' 65 | verbose_name_plural = 'Weight' 66 | 67 | def __str__(self): 68 | return f'{self.user.username} - {self.weight} kg on {self.entry_date}' 69 | -------------------------------------------------------------------------------- /foodtracker/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, Client 2 | from django.urls import reverse 3 | from .models import User, FoodCategory 4 | 5 | 6 | class UserModelUnitTestCase(TestCase): 7 | def setUp(self): 8 | self.user = User.objects.create(email='john@test.com', username='John', password='pass123') 9 | 10 | def test_user_model(self): 11 | data = self.user 12 | self.assertTrue(isinstance(data, User)) 13 | self.assertIsInstance(data, User) 14 | self.assertEqual(str(data.username), 'John') 15 | 16 | 17 | class FoodCategoryUnitTestCase(TestCase): 18 | def setUp(self): 19 | self.category = FoodCategory.objects.create(category_name='Vegetables') 20 | 21 | def test_food_category_model(self): 22 | data = self.category 23 | self.assertIsInstance(data, FoodCategory) 24 | self.assertEqual(str(data.category_name), 'Vegetables') 25 | 26 | 27 | class IndexRequestTestCase(TestCase): 28 | def setUp(self): 29 | self.client = Client() 30 | 31 | def test_index_view(self): 32 | # response = self.client.get('/') 33 | response = self.client.get(reverse('index')) 34 | self.assertEqual(response.status_code, 200) 35 | 36 | 37 | class LoginRequestTestCase(TestCase): 38 | def setUp(self): 39 | self.client = Client() 40 | 41 | def test_login_view(self): 42 | response = self.client.get(reverse('login')) 43 | self.assertEqual(response.status_code, 200) 44 | 45 | 46 | class RegisterRequestTestCase(TestCase): 47 | def setUp(self): 48 | self.client = Client() 49 | 50 | def test_login_view(self): 51 | response = self.client.get(reverse('register')) 52 | self.assertEqual(response.status_code, 200) 53 | 54 | 55 | class FoodListRequestTestCase(TestCase): 56 | def setUp(self): 57 | self.client = Client() 58 | 59 | def test_food_list_view(self): 60 | response = self.client.get(reverse('food_list')) 61 | self.assertEqual(response.status_code, 200) 62 | 63 | 64 | class CategoriesRequestTestCase(TestCase): 65 | def setUp(self): 66 | self.client = Client() 67 | 68 | def test_categories_view(self): 69 | response = self.client.get(reverse('categories_view')) 70 | self.assertEqual(response.status_code, 200) 71 | -------------------------------------------------------------------------------- /foodtracker/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = [ 7 | path('', views.index, name='index'), 8 | path('login', views.login_view, name='login'), 9 | path('logout', views.logout_view, name='logout'), 10 | path('register', views.register, name='register'), 11 | path('profile/weight', views.weight_log_view, name='weight_log'), 12 | path('profile/weight/delete/', views.weight_log_delete, name='weight_log_delete'), 13 | 14 | path('food/list', views.food_list_view, name='food_list'), 15 | path('food/add', views.food_add_view, name='food_add'), 16 | path('food/foodlog', views.food_log_view, name='food_log'), 17 | path('food/foodlog/delete/', views.food_log_delete, name='food_log_delete'), 18 | path('food/', views.food_details_view, name='food_details'), 19 | 20 | path('categories', views.categories_view, name='categories_view'), 21 | path('categories/', views.category_details_view, name='category_details_view'), 22 | ] 23 | -------------------------------------------------------------------------------- /foodtracker/views.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import authenticate, login, logout 3 | from django.contrib.auth.decorators import login_required 4 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 5 | from django.db import IntegrityError 6 | from django.http import HttpResponseRedirect 7 | from django.shortcuts import render, redirect 8 | from django.urls import reverse 9 | 10 | from .models import User, Food, FoodCategory, FoodLog, Image, Weight 11 | from .forms import FoodForm, ImageForm 12 | 13 | 14 | def index(request): 15 | ''' 16 | The default route which lists all food items 17 | ''' 18 | return food_list_view(request) 19 | 20 | 21 | def register(request): 22 | if request.method == 'POST': 23 | username = request.POST['username'] 24 | email = request.POST['email'] 25 | 26 | # Ensure password matches confirmation 27 | password = request.POST['password'] 28 | confirmation = request.POST['confirmation'] 29 | if password != confirmation: 30 | return render(request, 'register.html', { 31 | 'message': 'Passwords must match.', 32 | 'categories': FoodCategory.objects.all() 33 | }) 34 | 35 | # Attempt to create new user 36 | try: 37 | user = User.objects.create_user(username, email, password) 38 | user.save() 39 | except IntegrityError: 40 | return render(request, 'register.html', { 41 | 'message': 'Username already taken.', 42 | 'categories': FoodCategory.objects.all() 43 | }) 44 | login(request, user) 45 | return HttpResponseRedirect(reverse('index')) 46 | else: 47 | return render(request, 'register.html', { 48 | 'categories': FoodCategory.objects.all() 49 | }) 50 | 51 | 52 | def login_view(request): 53 | if request.method == 'POST': 54 | 55 | # Attempt to sign user in 56 | username = request.POST['username'] 57 | password = request.POST['password'] 58 | user = authenticate(request, username=username, password=password) 59 | 60 | # Check if authentication successful 61 | if user is not None: 62 | login(request, user) 63 | return HttpResponseRedirect(reverse('index')) 64 | else: 65 | return render(request, 'login.html', { 66 | 'message': 'Invalid username and/or password.', 67 | 'categories': FoodCategory.objects.all() 68 | }) 69 | else: 70 | return render(request, 'login.html', { 71 | 'categories': FoodCategory.objects.all() 72 | }) 73 | 74 | 75 | def logout_view(request): 76 | logout(request) 77 | return HttpResponseRedirect(reverse('index')) 78 | 79 | 80 | def food_list_view(request): 81 | ''' 82 | It renders a page that displays all food items 83 | Food items are paginated: 4 per page 84 | ''' 85 | foods = Food.objects.all() 86 | 87 | for food in foods: 88 | food.image = food.get_images.first() 89 | 90 | # Show 4 food items per page 91 | page = request.GET.get('page', 1) 92 | paginator = Paginator(foods, 4) 93 | try: 94 | pages = paginator.page(page) 95 | except PageNotAnInteger: 96 | pages = paginator.page(1) 97 | except EmptyPage: 98 | pages = paginator.page(paginator.num_pages) 99 | 100 | return render(request, 'index.html', { 101 | 'categories': FoodCategory.objects.all(), 102 | 'foods': foods, 103 | 'pages': pages, 104 | 'title': 'Food List' 105 | }) 106 | 107 | 108 | def food_details_view(request, food_id): 109 | ''' 110 | It renders a page that displays the details of a selected food item 111 | ''' 112 | if not request.user.is_authenticated: 113 | return HttpResponseRedirect(reverse('login')) 114 | 115 | food = Food.objects.get(id=food_id) 116 | 117 | return render(request, 'food.html', { 118 | 'categories': FoodCategory.objects.all(), 119 | 'food': food, 120 | 'images': food.get_images.all(), 121 | }) 122 | 123 | 124 | @login_required 125 | def food_add_view(request): 126 | ''' 127 | It allows the user to add a new food item 128 | ''' 129 | ImageFormSet = forms.modelformset_factory(Image, form=ImageForm, extra=2) 130 | 131 | if request.method == 'POST': 132 | food_form = FoodForm(request.POST, request.FILES) 133 | image_form = ImageFormSet(request.POST, request.FILES, queryset=Image.objects.none()) 134 | 135 | if food_form.is_valid() and image_form.is_valid(): 136 | new_food = food_form.save(commit=False) 137 | new_food.save() 138 | 139 | for food_form in image_form.cleaned_data: 140 | if food_form: 141 | image = food_form['image'] 142 | 143 | new_image = Image(food=new_food, image=image) 144 | new_image.save() 145 | 146 | return render(request, 'food_add.html', { 147 | 'categories': FoodCategory.objects.all(), 148 | 'food_form': FoodForm(), 149 | 'image_form': ImageFormSet(queryset=Image.objects.none()), 150 | 'success': True 151 | }) 152 | 153 | else: 154 | return render(request, 'food_add.html', { 155 | 'categories': FoodCategory.objects.all(), 156 | 'food_form': FoodForm(), 157 | 'image_form': ImageFormSet(queryset=Image.objects.none()), 158 | }) 159 | 160 | else: 161 | return render(request, 'food_add.html', { 162 | 'categories': FoodCategory.objects.all(), 163 | 'food_form': FoodForm(), 164 | 'image_form': ImageFormSet(queryset=Image.objects.none()), 165 | }) 166 | 167 | 168 | @login_required 169 | def food_log_view(request): 170 | ''' 171 | It allows the user to select food items and 172 | add them to their food log 173 | ''' 174 | if request.method == 'POST': 175 | foods = Food.objects.all() 176 | 177 | # get the food item selected by the user 178 | food = request.POST['food_consumed'] 179 | food_consumed = Food.objects.get(food_name=food) 180 | 181 | # get the currently logged in user 182 | user = request.user 183 | 184 | # add selected food to the food log 185 | food_log = FoodLog(user=user, food_consumed=food_consumed) 186 | food_log.save() 187 | 188 | else: # GET method 189 | foods = Food.objects.all() 190 | 191 | # get the food log of the logged in user 192 | user_food_log = FoodLog.objects.filter(user=request.user) 193 | 194 | return render(request, 'food_log.html', { 195 | 'categories': FoodCategory.objects.all(), 196 | 'foods': foods, 197 | 'user_food_log': user_food_log 198 | }) 199 | 200 | 201 | @login_required 202 | def food_log_delete(request, food_id): 203 | ''' 204 | It allows the user to delete food items from their food log 205 | ''' 206 | # get the food log of the logged in user 207 | food_consumed = FoodLog.objects.filter(id=food_id) 208 | 209 | if request.method == 'POST': 210 | food_consumed.delete() 211 | return redirect('food_log') 212 | 213 | return render(request, 'food_log_delete.html', { 214 | 'categories': FoodCategory.objects.all() 215 | }) 216 | 217 | 218 | @login_required 219 | def weight_log_view(request): 220 | ''' 221 | It allows the user to record their weight 222 | ''' 223 | if request.method == 'POST': 224 | 225 | # get the values from the form 226 | weight = request.POST['weight'] 227 | entry_date = request.POST['date'] 228 | 229 | # get the currently logged in user 230 | user = request.user 231 | 232 | # add the data to the weight log 233 | weight_log = Weight(user=user, weight=weight, entry_date=entry_date) 234 | weight_log.save() 235 | 236 | # get the weight log of the logged in user 237 | user_weight_log = Weight.objects.filter(user=request.user) 238 | 239 | return render(request, 'user_profile.html', { 240 | 'categories': FoodCategory.objects.all(), 241 | 'user_weight_log': user_weight_log 242 | }) 243 | 244 | 245 | @login_required 246 | def weight_log_delete(request, weight_id): 247 | ''' 248 | It allows the user to delete a weight record from their weight log 249 | ''' 250 | # get the weight log of the logged in user 251 | weight_recorded = Weight.objects.filter(id=weight_id) 252 | 253 | if request.method == 'POST': 254 | weight_recorded.delete() 255 | return redirect('weight_log') 256 | 257 | return render(request, 'weight_log_delete.html', { 258 | 'categories': FoodCategory.objects.all() 259 | }) 260 | 261 | 262 | def categories_view(request): 263 | ''' 264 | It renders a list of all food categories 265 | ''' 266 | return render(request, 'categories.html', { 267 | 'categories': FoodCategory.objects.all() 268 | }) 269 | 270 | 271 | def category_details_view(request, category_name): 272 | ''' 273 | Clicking on the name of any category takes the user to a page that 274 | displays all of the foods in that category 275 | Food items are paginated: 4 per page 276 | ''' 277 | if not request.user.is_authenticated: 278 | return HttpResponseRedirect(reverse('login')) 279 | 280 | category = FoodCategory.objects.get(category_name=category_name) 281 | foods = Food.objects.filter(category=category) 282 | 283 | for food in foods: 284 | food.image = food.get_images.first() 285 | 286 | # Show 4 food items per page 287 | page = request.GET.get('page', 1) 288 | paginator = Paginator(foods, 4) 289 | try: 290 | pages = paginator.page(page) 291 | except PageNotAnInteger: 292 | pages = paginator.page(1) 293 | except EmptyPage: 294 | pages = paginator.page(paginator.num_pages) 295 | 296 | return render(request, 'food_category.html', { 297 | 'categories': FoodCategory.objects.all(), 298 | 'foods': foods, 299 | 'foods_count': foods.count(), 300 | 'pages': pages, 301 | 'title': category.category_name 302 | }) 303 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 2 | coverage==7.3.0 3 | Django==4.2.4 4 | flake8==6.1.0 5 | mccabe==0.7.0 6 | Pillow==10.0.0 7 | psycopg2-binary==2.9.7 8 | pycodestyle==2.11.0 9 | pyflakes==3.1.0 10 | python-decouple==3.8 11 | pytz==2023.3 12 | sqlparse==0.4.4 13 | typing_extensions==4.1.1 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,*migrations*,*venv* 3 | max-line-length = 119 4 | indent-size = 2 -------------------------------------------------------------------------------- /static/css/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin: 0 0 90px; /* bottom = footer height */ 8 | font-family: 'Montserrat', sans-serif !important; 9 | } 10 | 11 | footer { 12 | position: absolute; 13 | left: 0; 14 | bottom: 0; 15 | height: 90px; 16 | width: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /static/images/FoodList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/static/images/FoodList.png -------------------------------------------------------------------------------- /static/images/FoodLog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/static/images/FoodLog.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/no_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BobsProgrammingAcademy/food-tracker/742e81c7896e46bfa6685a68d058a4b6b1ec9f45/static/images/no_image.png -------------------------------------------------------------------------------- /static/js/foodDetails.js: -------------------------------------------------------------------------------- 1 | let calories = 0, 2 | fat = 0, 3 | carbohydrates = 0, 4 | protein = 0; 5 | 6 | let caloriesValue = document.getElementById('calories_details').value; 7 | calories = parseFloat(caloriesValue); 8 | 9 | let fatValue = document.getElementById('fat_details').value; 10 | fat = parseFloat(fatValue); 11 | 12 | let carbohydratesValue = document.getElementById('carbohydrates_details').value; 13 | carbohydrates = parseFloat(carbohydratesValue); 14 | 15 | let proteinValue = document.getElementById('protein_details').value; 16 | protein = parseFloat(proteinValue); 17 | 18 | // Set new default font family and font color to mimic Bootstrap's default styling 19 | Chart.defaults.global.defaultFontFamily = 20 | 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; 21 | Chart.defaults.global.defaultFontColor = '#858796'; 22 | 23 | // Horizontal Bar Chart - Macronutrients breakdown 24 | const ctxBarChart = document.getElementById('myBarChart'); 25 | const myBarChart = new Chart(ctxBarChart, { 26 | type: 'horizontalBar', 27 | data: { 28 | labels: ['Fat', 'Carbs', 'Protein'], 29 | datasets: [ 30 | { 31 | data: [fat, carbohydrates, protein], 32 | backgroundColor: ['#e5a641', '#e94b43', '#419ad6'], 33 | barPercentage: 1, 34 | }, 35 | ], 36 | }, 37 | options: { 38 | responsive: true, 39 | legend: { 40 | display: false, 41 | position: 'bottom', 42 | fullWidth: true, 43 | labels: { 44 | boxWidth: 10, 45 | padding: 50, 46 | }, 47 | }, 48 | scales: { 49 | yAxes: [ 50 | { 51 | gridLines: { 52 | display: true, 53 | drawTicks: true, 54 | drawOnChartArea: false, 55 | }, 56 | ticks: { 57 | fontColor: '#555759', 58 | fontSize: 11, 59 | }, 60 | }, 61 | ], 62 | xAxes: [ 63 | { 64 | gridLines: { 65 | display: true, 66 | drawTicks: false, 67 | tickMarkLength: 5, 68 | drawBorder: false, 69 | }, 70 | ticks: { 71 | padding: 5, 72 | beginAtZero: true, 73 | fontColor: '#555759', 74 | fontSize: 11, 75 | min: 0, 76 | max: 100, 77 | maxTicksLimit: 10, 78 | padding: 10, 79 | // Include a 'g' in the ticks 80 | callback: function (value, index, values) { 81 | return value + 'g'; 82 | }, 83 | }, 84 | scaleLabel: { 85 | display: true, 86 | padding: 10, 87 | fontColor: '#555759', 88 | fontSize: 16, 89 | fontStyle: 700, 90 | labelString: 'Macronutrients (g) per 100 grams', 91 | }, 92 | }, 93 | ], 94 | }, 95 | tooltips: { 96 | enabled: false, 97 | }, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /static/js/foodLog.js: -------------------------------------------------------------------------------- 1 | const table = document.getElementById('foodtable'); 2 | let calories = 0, 3 | fat = 0, 4 | carbohydrates = 0, 5 | protein = 0; 6 | 7 | for (let i = 1; i < table.rows.length - 1; i++) { 8 | calories += parseFloat(table.rows[i].cells[1].innerHTML); 9 | 10 | fat += parseFloat(table.rows[i].cells[2].innerHTML); 11 | fat = Math.round(fat); 12 | 13 | carbohydrates += parseFloat(table.rows[i].cells[3].innerHTML); 14 | carbohydrates = Math.round(carbohydrates); 15 | 16 | protein += parseFloat(table.rows[i].cells[4].innerHTML); 17 | protein = Math.round(protein); 18 | } 19 | 20 | document.getElementById('totalCalories').innerHTML = '' + calories + ''; 21 | document.getElementById('totalFat').innerHTML = '' + fat + ''; 22 | document.getElementById('totalCarbohydrates').innerHTML = 23 | '' + carbohydrates + ''; 24 | document.getElementById('totalProtein').innerHTML = '' + protein + ''; 25 | 26 | let total = fat + carbohydrates + protein; 27 | 28 | let fatPercentage = Math.round((fat / total) * 100); 29 | let carbohydratesPercentage = Math.round((carbohydrates / total) * 100); 30 | let proteinPercentage = Math.round((protein / total) * 100); 31 | 32 | fatPercentage = fatPercentage ? fatPercentage : 0; 33 | carbohydratesPercentage = carbohydratesPercentage ? carbohydratesPercentage : 0; 34 | proteinPercentage = proteinPercentage ? proteinPercentage : 0; 35 | 36 | // Set new default font family and font color to mimic Bootstrap's default styling 37 | Chart.defaults.global.defaultFontFamily = 38 | 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; 39 | Chart.defaults.global.defaultFontColor = '#858796'; 40 | 41 | // Doughnut Chart - Macronutrients breakdown 42 | const ctxDoughnutChart = document.getElementById('myPieChart'); 43 | const myDoughnutChart = new Chart(ctxDoughnutChart, { 44 | type: 'doughnut', 45 | data: { 46 | labels: [ 47 | 'Fat ' + fatPercentage + '%', 48 | 'Carbs ' + carbohydratesPercentage + '%', 49 | 'Protein ' + proteinPercentage + '%', 50 | ], 51 | datasets: [ 52 | { 53 | data: [fatPercentage, carbohydratesPercentage, proteinPercentage], 54 | backgroundColor: ['#e5a641', '#55b560', '#419ad6'], 55 | }, 56 | ], 57 | }, 58 | options: { 59 | responsive: true, 60 | maintainAspectRatio: true, 61 | animation: { 62 | animateScale: true, 63 | }, 64 | plugins: { 65 | legend: { 66 | display: true, 67 | position: 'bottom', 68 | }, 69 | title: { 70 | display: true, 71 | text: 'Macronutrients Breakdown', 72 | font: { 73 | size: 20, 74 | }, 75 | }, 76 | datalabels: { 77 | display: true, 78 | color: '#fff', 79 | font: { 80 | weight: 'bold', 81 | size: 16, 82 | }, 83 | textAlign: 'center', 84 | }, 85 | }, 86 | }, 87 | }); 88 | 89 | // Calorie Goal Progress Bar 90 | 91 | let caloriePercentage = (calories / 2000) * 100; 92 | //document.getElementById('progressBar').setAttribute('style', 'width:' + caloriePercentage + '%'); 93 | 94 | $('.progress-bar').animate( 95 | { 96 | width: caloriePercentage + '%', 97 | }, 98 | 500 99 | ); 100 | let interval = setInterval(function () { 101 | $('.progress-bar').html(caloriePercentage + '%'); 102 | }, 500); 103 | -------------------------------------------------------------------------------- /static/js/foods.js: -------------------------------------------------------------------------------- 1 | const foods = [ 2 | { 3 | 'id': '1', 4 | 'food_name': 'Almonds', 5 | 'quantity': '100', 6 | 'calories': '579', 7 | 'fat': '49.93', 8 | 'carbohydrates': '21.55', 9 | 'protein': '21.15', 10 | 'food_category': 'Nuts and Seeds', 11 | }, 12 | { 13 | 'id': '2', 14 | 'food_name': 'Egg, boiled', 15 | 'quantity': '100', 16 | 'calories': '143', 17 | 'fat': '10', 18 | 'carbohydrates': '0.7', 19 | 'protein': '13', 20 | 'food_category': 'Meats, Fish and Eggs', 21 | }, 22 | { 23 | 'id': '3', 24 | 'food_name': 'Rolled Oats', 25 | 'quantity': '100', 26 | 'calories': '366', 27 | 'fat': '6.2', 28 | 'carbohydrates': '61', 29 | 'protein': '12', 30 | 'food_category': 'Breads and Grains', 31 | }, 32 | { 33 | 'id': '4', 34 | 'food_name': 'Apple', 35 | 'quantity': '100', 36 | 'calories': '52', 37 | 'fat': '0.2', 38 | 'carbohydrates': '14', 39 | 'protein': '0.3', 40 | 'food_category': 'Fruits', 41 | }, 42 | { 43 | '_id': '5', 44 | 'food_name': 'Banana', 45 | 'quantity': '100', 46 | 'calories': '89', 47 | 'fat': '0.3', 48 | 'carbohydrates': '23', 49 | 'protein': '1.1', 50 | 'food_category': 'Fruits', 51 | }, 52 | { 53 | 'id': '6', 54 | 'food_name': 'Green Peas', 55 | 'quantity': '100', 56 | 'calories': '81', 57 | 'fat': '0.4', 58 | 'carbohydrates': '14.46', 59 | 'protein': '5.42', 60 | 'food_category': 'Vegetables', 61 | }, 62 | { 63 | 'id': '7', 64 | 'food_name': 'Carrots, raw', 65 | 'quantity': '100', 66 | 'calories': '35', 67 | 'fat': '0.2', 68 | 'carbohydrates': '8.2', 69 | 'protein': '0.8', 70 | 'food_category': 'Vegetables', 71 | }, 72 | { 73 | 'id': '8', 74 | 'food_name': 'Cheese, Cheddar', 75 | 'quantity': '100', 76 | 'calories': '402', 77 | 'fat': '33', 78 | 'carbohydrates': '1.3', 79 | 'protein': '25', 80 | 'food_category': 'Dairy', 81 | }, 82 | { 83 | 'id': '9', 84 | 'food_name': 'Milk, 2% fat', 85 | 'quantity': '100', 86 | 'calories': '50', 87 | 'fat': '2', 88 | 'carbohydrates': '4.8', 89 | 'protein': '3.3', 90 | 'food_category': 'Dairy', 91 | }, 92 | { 93 | 'id': '10', 94 | 'food_name': 'Corn', 95 | 'quantity': '100', 96 | 'calories': '96', 97 | 'fat': '1.5', 98 | 'carbohydrates': '21', 99 | 'protein': '3.4', 100 | 'food_category': 'Vegetables', 101 | }, 102 | { 103 | 'id': '11', 104 | 'food_name': 'Cucumber', 105 | 'quantity': '100', 106 | 'calories': '15', 107 | 'fat': '0.1', 108 | 'carbohydrates': '3.6', 109 | 'protein': '0.7', 110 | 'food_category': 'Vegetables', 111 | }, 112 | { 113 | 'id': '12', 114 | 'food_name': 'Garlic', 115 | 'quantity': '100', 116 | 'calories': '149', 117 | 'fat': '0.5', 118 | 'carbohydrates': '33', 119 | 'protein': '6.4', 120 | 'food_category': 'Vegetables', 121 | }, 122 | { 123 | 'id': '13', 124 | 'food_name': 'Green Tea', 125 | 'quantity': '100', 126 | 'calories': '1', 127 | 'fat': '0', 128 | 'carbohydrates': '0.2', 129 | 'protein': '0', 130 | 'food_category': 'Beverages', 131 | }, 132 | { 133 | 'id': '14', 134 | 'food_name': 'Cheese, Mozzarella', 135 | 'quantity': '100', 136 | 'calories': '300', 137 | 'fat': '22', 138 | 'carbohydrates': '2.2', 139 | 'protein': '22', 140 | 'food_category': 'Dairy', 141 | }, 142 | { 143 | 'id': '15', 144 | 'food_name': 'Orange', 145 | 'quantity': '100', 146 | 'calories': '47', 147 | 'fat': '0.1', 148 | 'carbohydrates': '12', 149 | 'protein': '0.9', 150 | 'food_category': 'Fruits', 151 | }, 152 | { 153 | 'id': '16', 154 | 'food_name': 'Pistachios', 155 | 'quantity': '100', 156 | 'calories': '562', 157 | 'fat': '45', 158 | 'carbohydrates': '28', 159 | 'protein': '20', 160 | 'food_category': 'Nuts and Seeds', 161 | }, 162 | { 163 | 'id': '17', 164 | 'food_name': 'Sunflower Seeds', 165 | 'quantity': '100', 166 | 'calories': '584', 167 | 'fat': '51', 168 | 'carbohydrates': '20', 169 | 'protein': '21', 170 | 'food_category': 'Nuts and Seeds', 171 | }, 172 | { 173 | 'id': '18', 174 | 'food_name': 'Tomatoes', 175 | 'quantity': '100', 176 | 'calories': '18', 177 | 'fat': '0.2', 178 | 'carbohydrates': '3.9', 179 | 'protein': '0.9', 180 | 'food_category': 'Vegetables', 181 | }, 182 | { 183 | 'id': '19', 184 | 'food_name': 'Tuna', 185 | 'quantity': '100', 186 | 'calories': '132', 187 | 'fat': '1.3', 188 | 'carbohydrates': '0', 189 | 'protein': '28', 190 | 'food_category': 'Meats, Fish and Eggs', 191 | }, 192 | { 193 | 'id': '20', 194 | 'food_name': 'Salmon, Atlantic, raw', 195 | 'quantity': '100', 196 | 'calories': '208', 197 | 'fat': '13', 198 | 'carbohydrates': '0', 199 | 'protein': '20', 200 | 'food_category': 'Meats, Fish and Eggs', 201 | }, 202 | ] 203 | 204 | export default foods; -------------------------------------------------------------------------------- /static/js/userProfile.js: -------------------------------------------------------------------------------- 1 | const table = document.getElementById('weightable'); 2 | 3 | let recorded_weight = []; 4 | let recorded_date = []; 5 | 6 | for (let i = 1; i < table.rows.length; i++) { 7 | recorded_weight.push([parseFloat(table.rows[i].cells[0].innerHTML)]); 8 | 9 | recorded_date.push([table.rows[i].cells[1].innerHTML]); 10 | } 11 | 12 | let values = recorded_weight.flat(); 13 | 14 | // Set new default font family and font color to mimic Bootstrap's default styling 15 | Chart.defaults.global.defaultFontFamily = 16 | 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; 17 | Chart.defaults.global.defaultFontColor = '#858796'; 18 | 19 | // Area Chart - Weight History 20 | const ctxAreaChart = document.getElementById('myChart'); 21 | const myAreaChart = new Chart(ctxAreaChart, { 22 | type: 'line', 23 | data: { 24 | labels: [...recorded_date], 25 | datasets: [ 26 | { 27 | label: 'Weight', 28 | data: values, 29 | lineTension: 0.3, 30 | backgroundColor: 'rgba(2,117,216,0.2)', 31 | borderColor: 'rgba(2,117,216,1)', 32 | pointRadius: 5, 33 | pointBackgroundColor: 'rgba(2,117,216,1)', 34 | pointBorderColor: 'rgba(255,255,255,0.8)', 35 | pointHoverRadius: 5, 36 | pointHoverBackgroundColor: 'rgba(2,117,216,1)', 37 | pointHitRadius: 50, 38 | pointBorderWidth: 2, 39 | }, 40 | ], 41 | }, 42 | options: { 43 | scales: { 44 | xAxes: [ 45 | { 46 | ticks: { 47 | autoSkip: false, 48 | maxRotation: 60, 49 | minRotation: 60, 50 | }, 51 | gridLines: { 52 | display: true, 53 | }, 54 | scaleLabel: { 55 | display: true, 56 | padding: 10, 57 | fontColor: '#555759', 58 | fontSize: 16, 59 | fontStyle: 700, 60 | labelString: 'Date', 61 | }, 62 | }, 63 | ], 64 | yAxes: [ 65 | { 66 | ticks: { 67 | min: 0, 68 | max: 120, 69 | maxTicksLimit: 12, 70 | padding: 10, 71 | // Include a 'kg' in the ticks 72 | callback: function (value, index, values) { 73 | return value + 'kg'; 74 | }, 75 | }, 76 | gridLines: { 77 | color: 'rgba(0, 0, 0, .125)', 78 | }, 79 | scaleLabel: { 80 | display: true, 81 | padding: 10, 82 | fontColor: '#555759', 83 | fontSize: 16, 84 | fontStyle: 700, 85 | labelString: 'Weight in kg', 86 | }, 87 | }, 88 | ], 89 | }, 90 | legend: { 91 | display: false, 92 | }, 93 | }, 94 | }); 95 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% block title %}Food Tracker{% endblock %} 25 | 26 | 27 | 28 | 29 | 115 | 116 |
117 |
118 | {% block body %} 119 | {% endblock %} 120 |
121 |
122 | 123 |
124 |
125 |
126 | Copyright © Bob's Programming Academy. 127 |
128 |
129 |
130 | 131 | {% block script %} 132 | 133 | {% endblock %} 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /templates/categories.html: -------------------------------------------------------------------------------- 1 | {% block categories %} 2 | 3 | {% for category in categories %} 4 | 5 | {{ category }}   6 | 7 | 8 | {{ category.count_food_by_category }} 9 | 10 | 11 | {% endfor %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /templates/food.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %}Food Tracker | {{ food.food_name }}{% endblock %} 6 | 7 | {% block body %} 8 |
9 |
10 | Go Back 11 | 12 |

{{ food.food_name }}

13 |
Category: {{ food.category }}
14 | 15 |
16 |
17 | 18 | {% for image in images %} 19 | 20 | 21 | food image 26 | 27 | {% endfor %} 28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | Calories per {{ food.quantity|floatformat:0 }} grams: 36 |
37 |

{{ food.calories }}

38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | Macronutrients (g) per {{ food.quantity|floatformat:0 }} grams: 52 |
53 |
54 | 55 |
56 |

Fat:

57 |

{{ food.fat }}

58 |
59 | 60 |
61 |

Carbs:

62 |

{{ food.carbohydrates }}

63 |
64 | 65 |
66 |

Protein:

67 |

{{ food.protein }}

68 |
69 |
70 |
71 | 72 |
73 |
74 |

Macronutrients breakdown

75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 |
86 | 87 |
88 |
89 | {% endblock %} 90 | 91 | {% block script %} 92 | 93 | 94 | 95 | 96 | 97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /templates/food_add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | Add Food Item{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 | {% if success %} 10 |
11 | 15 | {% else %} 16 |
17 |

Add Food Item

18 |
19 | 20 |
21 |

Enter the details of a new food item

22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 | {% csrf_token %} 32 |
33 | {{ food_form.as_p }} 34 | 35 | {{ image_form.management_form }} 36 | 37 | {% for form in image_form %} 38 | {{ form.as_p }} 39 | {% endfor %} 40 | 41 | 42 | Cancel 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 | {% endif %} 52 | 53 |
54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /templates/food_category.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | {{ title }} {% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 |

Category: {{ title }}

10 | 11 |
12 | 13 | {% if foods_count > 0 %} 14 | 15 | {% for food in pages %} 16 |
17 |
18 | 19 | 20 | 21 | food image 26 | 27 | 28 |
29 |

30 | 31 | {{ food.food_name }} 32 |

33 | 34 |

35 |

36 | 37 | Category: {{ food.category }} 38 |
39 |

40 | 41 |

42 |

43 | 44 | {{ food.calories}} calories in {{ food.quantity|floatformat:0 }} grams 45 |
46 |

47 | 48 |

49 |

54 |

55 |
56 | 57 |
58 |
59 | 60 | {% endfor %} 61 | 62 | {% if pages.has_other_pages %} 63 |
64 |
    65 | {% if pages.has_previous %} 66 |
  • 67 | « 68 |
  • 69 | {% else %} 70 |
  • 71 | « 72 |
  • 73 | {% endif %} 74 | {% for i in pages.paginator.page_range %} 75 | {% if pages.number == i %} 76 |
  • 77 | {{ i }} 78 |
  • 79 | {% else %} 80 |
  • 81 | {{ i }} 82 |
  • 83 | {% endif %} 84 | {% endfor %} 85 | {% if pages.has_next %} 86 |
  • 87 | » 88 |
  • 89 | {% else %} 90 |
  • 91 | » 92 |
  • 93 | {% endif %} 94 |
95 |
96 | {% endif %} 97 | 98 | {% else %} 99 |
100 | 103 | {% endif %} 104 | 105 |
106 |
107 | 108 | {% endblock %} -------------------------------------------------------------------------------- /templates/food_log.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %}Food Tracker | Food Log{% endblock %} 6 | 7 | {% block body %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
Select food to add to the Food Log
20 |
21 |
22 | 23 |
24 |
25 | {% csrf_token %} 26 |
27 |
28 |
29 | 38 |
39 |
40 | 43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 |
55 |
Food consumed today
56 |
{% now 'D, jS F Y' %}
57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {% for food_item in user_food_log %} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 85 | 86 | {% endfor %} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
Food ItemCaloriesFat (g) in 100gCarbs (g) in 100gProtein (g) in 100g
{{ food_item.food_consumed.food_name }}{{ food_item.food_consumed.calories }}{{ food_item.food_consumed.fat }}{{ food_item.food_consumed.carbohydrates }}{{ food_item.food_consumed.protein }} 81 | 82 | 83 | 84 |
Total
98 | 99 |
100 |
101 | 102 |
103 | 104 |
105 | 106 |
107 |
108 |
Daily Calorie Goal - 2,000 calories
109 |
110 |
111 | 112 |
113 |
0%
122 |
123 | 124 |
125 |
126 | 127 |
128 |
129 |
Macronutrients Breakdown
130 |
131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 | {% endblock %} 147 | 148 | {% block script %} 149 | 150 | 151 | 152 | 153 | 154 | 155 | {% endblock %} 156 | -------------------------------------------------------------------------------- /templates/food_log_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | Food Log{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 |

Food Log

10 | 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | {% csrf_token %} 19 | 20 | Are you sure you want to delete this food item? 21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | {{ title }}{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 |

{{ title }}

10 | 11 |
12 | 13 | {% for food in pages %} 14 |
15 |
16 | 17 | 18 | 19 | food image 24 | 25 | 26 |
27 |

28 | 29 | {{ food.food_name }} 30 |

31 | 32 |

33 |

34 | 35 | Category: {{ food.category }} 36 |
37 |

38 | 39 |

40 |

41 | 42 | {{ food.calories}} calories in {{ food.quantity|floatformat:0 }} grams 43 |
44 |

45 | 46 |

47 |

52 |

53 |
54 | 55 |
56 |
57 | 58 | {% endfor %} 59 | 60 | {% if pages.has_other_pages %} 61 |
62 |
    63 | {% if pages.has_previous %} 64 |
  • 65 | « 66 |
  • 67 | {% else %} 68 |
  • 69 | « 70 |
  • 71 | {% endif %} 72 | 73 | {% for i in pages.paginator.page_range %} 74 | {% if pages.number == i %} 75 |
  • 76 | {{ i }} 77 |
  • 78 | {% else %} 79 |
  • 80 | {{ i }} 81 |
  • 82 | {% endif %} 83 | {% endfor %} 84 | 85 | {% if pages.has_next %} 86 |
  • 87 | » 88 |
  • 89 | {% else %} 90 |
  • 91 | » 92 |
  • 93 | {% endif %} 94 |
95 |
96 | {% endif %} 97 | 98 |
99 |
100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | Login{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 |
9 |

Login

10 |
11 | 12 | {% if message %} 13 |
{{ message }}
14 | {% endif %} 15 | 16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 | {% csrf_token %} 25 |
26 | 29 | 30 |
31 | 32 |
33 | 36 | 37 |
38 | 39 |
40 | 43 |
44 |
45 | 46 |
47 | Don't have an account? 48 | Register here 49 | 50 |
51 | 52 |
53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | {% endblock %} -------------------------------------------------------------------------------- /templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | Registration{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 |
10 |

Register

11 |
12 | 13 | {% if message %} 14 |
{{ message }}
15 | {% endif %} 16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 | {% csrf_token %} 26 |
27 | 30 | 31 |
32 | 33 |
34 | 37 | 38 |
39 | 40 |
41 | 44 | 45 |
46 | 47 |
48 | 51 | 52 |
53 | 54 |
55 | 58 |
59 | 60 |
61 | 62 |
63 | Already have an account? 64 | Log In here 65 | 66 |
67 | 68 |
69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 | {% endblock %} -------------------------------------------------------------------------------- /templates/user_profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %}Food Tracker | Food Log{% endblock %} 6 | 7 | {% block body %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |

Hi, {{ user.username }}!

20 |
21 |
22 | 23 |
24 |

25 | Username: {{ user.username }} 26 |

27 |
28 | 29 |
30 |

31 | Email: {{ user.email }} 32 |

33 |
34 | 35 |
36 |

37 | Date joined: {{ user.date_joined }} 38 |

39 |
40 | 41 |
42 |

43 | Last login: {{ user.last_login }} 44 |

45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 |
53 |

Record Your Weight

54 |
55 |
56 | 57 |
58 | 59 |
60 | {% csrf_token %} 61 |
62 |
63 |
64 | 67 | 73 |
74 | 75 |
76 | 79 | 91 |
92 | 93 |
94 | 97 |
98 |
99 |
100 |
101 | 102 |
103 | 104 |
105 |
106 | 107 |
108 |
109 |

Weight Log

110 |
111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | {% for weight_record in user_weight_log %} 124 | 125 | 126 | 127 | 135 | 136 | {% endfor %} 137 | 138 |
Weight in kgDate
{{ weight_record.weight }}{{ weight_record.entry_date|date:'Y-m-d' }} 128 | 129 | 130 | 131 | 132 | 133 | 134 |
139 | 140 |
141 |
142 | 143 |
144 | 145 |
146 |
147 |
148 |

Weight History

149 |
150 | 151 |
152 |
153 | 154 |
155 |
156 |
157 |
158 | 159 |
160 |
161 |
162 | 163 |
164 |
165 | {% endblock %} 166 | 167 | {% block script %} 168 | 169 | 170 | 171 | 172 | 173 | {% endblock %} 174 | -------------------------------------------------------------------------------- /templates/weight_log_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Food Tracker | Weight Log{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 |

Weight Log

10 | 11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | {% csrf_token %} 19 | 20 | Are you sure you want to delete this record? 21 | 24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | {% endblock %} --------------------------------------------------------------------------------