Compare commits

..

No commits in common. "main" and "demo1" have entirely different histories.
main ... demo1

60 changed files with 493 additions and 718 deletions

View file

@ -1,23 +0,0 @@
kind: pipeline
type: exec
name: default
platform:
os: linux
arch: amd64
steps:
- name: install deps
commands:
- dnf install -y python3 python3-pip
- pip3 install -U Django coverage flake8 pylint django-coverage-plugin pylint-django
- name: run unittests
commands:
- coverage run --source='.' manage.py test --noinput --parallel
- name: run flake8
commands:
- flake8
- name: run pylint
commands:
- pylint --rcfile=.pylintrc -- **/*.py

View file

@ -1,47 +0,0 @@
name: "CodeQL"
on:
push:
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule:
- cron: '0 9 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['python']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -1,49 +0,0 @@
# This workflow integrates a collection of open source static analysis tools
# with GitHub code scanning. For documentation, or to provide feedback, visit
# https://github.com/github/ossar-action
name: OSSAR
on:
push:
pull_request:
jobs:
OSSAR-Scan:
# OSSAR runs on windows-latest.
# ubuntu-latest and macos-latest support coming soon
runs-on: windows-latest
steps:
# Checkout your code repository to scan
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Ensure a compatible version of dotnet is installed.
# The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201.
# A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action.
# Remote agents already have a compatible version of dotnet installed and this step may be skipped.
# For local agents, ensure dotnet version 3.1.201 or later is installed by including this action:
# - name: Install .NET
# uses: actions/setup-dotnet@v1
# with:
# dotnet-version: '3.1.x'
# Run open source static analysis tools
- name: Run OSSAR
uses: github/ossar-action@v1
id: ossar
# Upload results to the Security tab
- name: Upload OSSAR results
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: ${{ steps.ossar.outputs.sarifFile }}

3
.gitignore vendored
View file

@ -102,6 +102,3 @@ venv.bak/
# mypy
.mypy_cache/
.DS_Store

View file

@ -1,15 +0,0 @@
[MASTER]
load-plugins=pylint_django
django-settings-module=im.settings
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=missing-docstring,unnecessary-pass
[DESIGN]
max-parents=13
[TYPECHECK]
generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete

View file

@ -1,18 +1,14 @@
[![Build Status](https://drone.caret.be/api/badges/jens/im/status.svg)](https://drone.caret.be/jens/im)
# im
Pantry inventory management
# Inventory management system written in django - python
The object of this django app is to keep track of which goods you own, when they will expire, when you should consume them and when you should buy more.
The object of this django app is to keep track of which goods you own, when they will expire, when you should consume them and when you shoud buy more.
## features
- Enter items in your pantry (or other locations, with their location) with their expiry date
- Items can have a minimum quantity you always want to keep in the pantry
- a shopping list is automatically populated with items whose quantity is below this minimum quantity
- you can add one-off items to this shopping list that are not tracked in the inventory.
- you can add one off items to this shopping list that are not tracked in the inventory.
- You get an overview of items per expiry date so you know what to consume first (or throw away)
- Extensive search and filtering and grouping on locations, categories, units, expiry date using the auto generated django admin
@ -24,7 +20,7 @@ The object of this django app is to keep track of which goods you own, when they
```
dnf install -y python3-pip git
pip3 install django
git clone https://gitea.caret.be/jens/im.git
git clone git@github.com:JensTimmerman/im.git
cd im
python3 manage.py migrate
@ -44,7 +40,6 @@ Now you are running the development server, Visit http://127.0.0.1:8000/admin/
# installation
If you use ansible: see https://gitea.caret.be/jens/ansible-role-im
```
dnf install python3-pip git
pip3 install django psycopg2 gunicorn
@ -73,6 +68,10 @@ DATABASES = {
clear migrations and start from empty db
```
rm inventory/migrations/* -rf
python3 manage.py migrate
python3 manage.py createsuperuser
python3 ./manage.py collectstatic
```
@ -157,7 +156,7 @@ browse to https://im.yourdomain
# feature requests
## High
- Add items per location, click a location and then start adding items with the location list prepopulated
- Add itmes per location, click a location and then start adding items with the location list prepopulated
- auto refresh dropdown boxes after items have been added in admin view, so page reload is not needed
- auto parse dates in different date formats when given in a datefield. e.g. 20 12 2020 or 2020 12 20 to 2020-12-20
- also make ipad show the numbered keyboard here
@ -199,9 +198,9 @@ browse to https://im.yourdomain
- easily deploy somewhere
## low
- add recipes
- shopping list created based on recipe ingredients
- auto proposal of recipe based on next expiry dates
- add recepies
- shopping list created based on recepy ingredients
- auto proposal of recepy based on next expiry dates
- offer to buy things that have been on shopping list for a while online (in bulk/aggregated)
- 3d view of where things are in space
- vr to help locate things

View file

@ -25,19 +25,14 @@ SECRET_KEY = '-h%7n38#ij*7$pzkv=8-+9axa6o6fk9e4z3x676774f&06-di9'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [
'0.0.0.0',
]
ALLOWED_HOSTS = []
LOGIN_REDIRECT_URL = "im:index"
LOGOUT_REDIRECT_URL = "im:index"
# Application definition
INSTALLED_APPS = [
'users.apps.UsersConfig',
'django.contrib.auth',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
@ -69,7 +64,6 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'debug': DEBUG,
},
},
]
@ -106,10 +100,6 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# enabling this may speed up testing
# PASSWORD_HASHERS = [
# 'django.contrib.auth.hashers.MD5PasswordHasher',
# ]
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
@ -129,7 +119,3 @@ USE_TZ = True
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/'
EMAIL_HOST = 'mail.caret.be'
EMAIL_PORT = '25'

View file

@ -26,14 +26,10 @@ SECRET_KEY = '{{im_secret_key}}'
DEBUG = False
ALLOWED_HOSTS = [
'{{im_domain}}'
{{im_domain}}
]
ADMINS = [('admin', '{{im_admin_email}}')]
EMAIL_HOST = '{{im_mail_server}}'
# Application definition
INSTALLED_APPS = [
@ -86,7 +82,7 @@ DATABASES = {
'NAME': '{{im_db_name}}',
'USER': '{{im_db_user}}',
'PASSWORD': '{{im_db_password}}',
'HOST': '{{im_db_server}}',
'HOST': '{{im_db_server_ip}}',
'PORT': '{{im_db_port}}',
}
}

View file

@ -16,11 +16,7 @@ Including another URLconf
from django.contrib import admin
from django.urls import include, path
from users.views import register
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),
path("register/", register, name="register"),
path('', include('inventory.urls')),
]

View file

@ -1 +0,0 @@
[{"model": "inventory.category", "pk": 1, "fields": {"name": "UNCATEGORIZED"}}, {"model": "inventory.category", "pk": 2, "fields": {"name": "GENRAL_STOCK"}}, {"model": "inventory.category", "pk": 3, "fields": {"name": "CANS"}}, {"model": "inventory.category", "pk": 4, "fields": {"name": "BREAKFAST"}}, {"model": "inventory.category", "pk": 5, "fields": {"name": "SAVORIES"}}, {"model": "inventory.category", "pk": 6, "fields": {"name": "SWEETS"}}, {"model": "inventory.category", "pk": 7, "fields": {"name": "SNACKS"}}, {"model": "inventory.category", "pk": 8, "fields": {"name": "SPICES"}}, {"model": "inventory.category", "pk": 9, "fields": {"name": "DRINKS"}}, {"model": "inventory.unit", "pk": 1, "fields": {"name": "Mililiter"}}, {"model": "inventory.unit", "pk": 2, "fields": {"name": "Grams"}}, {"model": "inventory.unit", "pk": 3, "fields": {"name": "Roll"}}, {"model": "inventory.unit", "pk": 4, "fields": {"name": "Tube"}}, {"model": "inventory.unit", "pk": 5, "fields": {"name": "Bag"}}, {"model": "inventory.unit", "pk": 6, "fields": {"name": "Bar"}}, {"model": "inventory.unit", "pk": 7, "fields": {"name": "Pack"}}, {"model": "inventory.location", "pk": 1, "fields": {"name": "Caret", "in_location": null}}]

View file

@ -9,6 +9,7 @@ class PantryItemInLine(admin.TabularInline):
extra = 1
class LocationInLine(admin.TabularInline):
model = Location
extra = 1
@ -16,20 +17,16 @@ class LocationInLine(admin.TabularInline):
def upper_case_name(obj):
return obj.name.upper()
upper_case_name.short_description = 'Name'
def capitalize_name(obj):
return obj.name.capitalize()
upper_case_name.short_description = 'Name'
class PantryItemLineAdmin(admin.ModelAdmin):
list_filter = ['expiry_date', 'pantry_item__unit', 'pantry_item', 'pantry_item__min_quantity', 'location']
list_filter = ['expiry_date', 'pantry_item__unit', 'pantry_item', 'pantry_item__min_quantity']
search_fields = ['info', 'pantry_item__name', 'pantry_item__info']
autocomplete_fields = ['pantry_item']
@ -62,12 +59,13 @@ class LocationAdmin(AutocompleteAdmin):
class PantryItemAdmin(admin.ModelAdmin):
list_filter = ['category', 'unit', 'min_quantity']
list_filter = ['category', 'unit', 'min_quantity', 'location']
search_fields = ['info', 'name', 'category__name', 'unit__name']
autocomplete_fields = ['category', 'unit']
inlines = [PantryItemInLine]
autocomplete_fields = ['category', ]
# TODO: make category a model
#autocomplete_fields = ['category',]
fields = (
'name',
'category',
@ -95,7 +93,6 @@ class ShoppingListItemAdmin(PantryItemAdmin):
'info',
)
admin.site.register(PantryItem, PantryItemAdmin)
admin.site.register(ShoppingListItem, ShoppingListItemAdmin)
admin.site.register(PantryItemLine, PantryItemLineAdmin)

View file

@ -1,4 +1,4 @@
# Generated by Django 3.1.3 on 2020-11-08 15:14
# Generated by Django 2.1.1 on 2018-09-16 10:49
from django.db import migrations, models
import django.db.models.deletion
@ -12,61 +12,20 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='Location',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=200, null=True)),
('in_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.location')),
],
),
migrations.CreateModel(
name='PantryItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('min_quantity', models.IntegerField(default=1)),
('expiry_duration', models.IntegerField(blank=True, null=True)),
('name', models.CharField(max_length=200)),
('info', models.CharField(blank=True, max_length=200, null=True)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.category')),
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.location')),
],
),
migrations.CreateModel(
name='Unit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='ShoppingListItem',
fields=[
('pantryitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='inventory.pantryitem')),
],
bases=('inventory.pantryitem',),
),
migrations.CreateModel(
name='PantryItemLine',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.IntegerField(default=1)),
('expiry_date', models.DateField(blank=True, null=True)),
('size', models.IntegerField(default=1)),
('info', models.CharField(blank=True, max_length=200, null=True)),
('pantry_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.pantryitem')),
('expiry_date', models.DateTimeField()),
('pantry_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.PantryItem')),
],
),
migrations.AddField(
model_name='pantryitem',
name='unit',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.unit'),
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 2.1.1 on 2018-09-16 11:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='pantryitem',
name='category',
field=models.CharField(choices=[('UC', 'UNCATEGORIZED'), ('GS', 'GENRAL_STOCK'), ('CANS', 'CANS'), ('BR', 'BREAKFAST'), ('SAV', 'SAVORIES'), ('SW', 'SWEETS'), ('SN', 'SNACKS'), ('SP', 'SPICES')], default='UN', max_length=200),
preserve_default=False,
),
migrations.AlterField(
model_name='pantryitemline',
name='expiry_date',
field=models.DateField(),
),
migrations.AlterField(
model_name='pantryitemline',
name='pantry_item',
field=models.ForeignKey(default='UN', on_delete=django.db.models.deletion.PROTECT, to='inventory.PantryItem'),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.4 on 2021-07-04 18:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='pantryitem',
name='location',
),
migrations.AddField(
model_name='pantryitemline',
name='location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.location'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.1.1 on 2018-09-16 11:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_auto_20180916_1110'),
]
operations = [
migrations.AddField(
model_name='pantryitem',
name='min_quantity',
field=models.IntegerField(default=1),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1.1 on 2018-09-16 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_pantryitem_min_quantity'),
]
operations = [
migrations.AddField(
model_name='pantryitemline',
name='size',
field=models.CharField(max_length=20, null=True),
),
migrations.AlterField(
model_name='pantryitem',
name='category',
field=models.CharField(choices=[('UC', 'UNCATEGORIZED'), ('GS', 'GENRAL_STOCK'), ('CANS', 'CANS'), ('BR', 'BREAKFAST'), ('SAV', 'SAVORIES'), ('SW', 'SWEETS'), ('SN', 'SNACKS'), ('SP', 'SPICES'), ('DR', 'DRINKS')], max_length=200),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 2.1.1 on 2018-09-16 11:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_auto_20180916_1127'),
]
operations = [
migrations.AddField(
model_name='pantryitem',
name='info',
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name='pantryitemline',
name='info',
field=models.CharField(max_length=200, null=True),
),
migrations.AlterField(
model_name='pantryitemline',
name='size',
field=models.IntegerField(default=1),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1.1 on 2018-09-16 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_auto_20180916_1154'),
]
operations = [
migrations.AddField(
model_name='pantryitem',
name='unit',
field=models.CharField(choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
),
migrations.AddField(
model_name='pantryitemline',
name='unit',
field=models.CharField(choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1.1 on 2018-09-16 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_auto_20180916_1156'),
]
operations = [
migrations.AlterField(
model_name='pantryitem',
name='info',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterField(
model_name='pantryitemline',
name='info',
field=models.CharField(blank=True, max_length=200, null=True),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1.1 on 2018-09-16 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_auto_20180916_1203'),
]
operations = [
migrations.AlterField(
model_name='pantryitem',
name='unit',
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
),
migrations.AlterField(
model_name='pantryitemline',
name='unit',
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 2.1.1 on 2018-09-16 12:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_auto_20180916_1205'),
]
operations = [
migrations.AlterField(
model_name='pantryitem',
name='unit',
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll')], max_length=20, null=True),
),
migrations.AlterField(
model_name='pantryitemline',
name='expiry_date',
field=models.DateField(blank=True, null=True),
),
migrations.AlterField(
model_name='pantryitemline',
name='unit',
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll')], max_length=20, null=True),
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 2.1.1 on 2018-09-16 12:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0009_auto_20180916_1219'),
]
operations = [
migrations.AlterField(
model_name='pantryitem',
name='min_quantity',
field=models.DecimalField(decimal_places=3, default=1, max_digits=32),
),
migrations.AlterField(
model_name='pantryitem',
name='unit',
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll'), ('Tube', 'Tube'), ('Bag', 'Bag')], max_length=20, null=True),
),
migrations.AlterField(
model_name='pantryitemline',
name='size',
field=models.DecimalField(decimal_places=3, default=1, max_digits=32),
),
migrations.AlterField(
model_name='pantryitemline',
name='unit',
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll'), ('Tube', 'Tube'), ('Bag', 'Bag')], max_length=20, null=True),
),
]

View file

@ -0,0 +1,57 @@
# Generated by Django 2.1.1 on 2018-09-23 20:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0010_auto_20180916_1252'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='Unit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('unit', models.CharField(max_length=50)),
],
),
migrations.RemoveField(
model_name='pantryitemline',
name='unit',
),
migrations.AlterField(
model_name='pantryitem',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.Category'),
),
migrations.AlterField(
model_name='pantryitem',
name='min_quantity',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='pantryitem',
name='unit',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Unit'),
),
migrations.AlterField(
model_name='pantryitemline',
name='pantry_item',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.PantryItem'),
),
migrations.AlterField(
model_name='pantryitemline',
name='size',
field=models.IntegerField(default=1),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.1.1 on 2018-09-23 20:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0011_auto_20180923_2025'),
]
operations = [
migrations.RenameField(
model_name='unit',
old_name='unit',
new_name='name',
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.1.1 on 2018-09-27 17:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0012_auto_20180923_2037'),
]
operations = [
migrations.AddField(
model_name='pantryitem',
name='expiry_duration',
field=models.DurationField(blank=True, null=True),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 2.1.1 on 2018-09-30 14:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0013_pantryitem_expiry_duration'),
]
operations = [
migrations.CreateModel(
name='Location',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=200, null=True)),
('in_location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.Location')),
],
),
migrations.AlterField(
model_name='pantryitem',
name='expiry_duration',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='pantryitem',
name='location',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.1.1 on 2018-09-30 14:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0014_auto_20180930_1406'),
]
operations = [
migrations.AlterField(
model_name='location',
name='in_location',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.1.1 on 2018-09-30 14:09
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0015_auto_20180930_1408'),
]
operations = [
migrations.AlterField(
model_name='location',
name='in_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
),
]

View file

@ -0,0 +1,26 @@
# Generated by Django 2.1.2 on 2018-10-07 11:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0016_auto_20180930_1409'),
]
operations = [
migrations.CreateModel(
name='ShoppingListItem',
fields=[
('pantryitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='inventory.PantryItem')),
],
bases=('inventory.pantryitem',),
),
migrations.AlterField(
model_name='pantryitem',
name='location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.1.2 on 2018-10-09 10:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0017_auto_20181007_1108'),
]
operations = [
migrations.AlterField(
model_name='pantryitem',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Category'),
),
]

View file

@ -39,7 +39,7 @@ class Unit(models.Model):
class Location(models.Model):
"""Location for a pantry item line"""
name = models.CharField(max_length=200)
name = models.CharField(max_length=200, null=True, blank=True)
in_location = models.ForeignKey("self", on_delete=models.PROTECT, null=True, blank=True)
def __str__(self):
@ -47,9 +47,6 @@ class Location(models.Model):
return self.name + ' ' + str(self.in_location)
return self.name
class Meta:
unique_together = ('name', 'in_location')
class PantryItem(models.Model):
"""A think you keep in your pantry """
@ -60,9 +57,12 @@ class PantryItem(models.Model):
# if expiry duration is set the expiration date for a pantryitemline will be set to now + duration on save
# represents days
expiry_duration = models.IntegerField(null=True, blank=True)
name = models.CharField(max_length=200)
name = models.CharField(max_length=200)
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True)
info = models.CharField(max_length=200, null=True, blank=True)
# location is saved on a per item base, not itemline
# you can have multiple pantries with subpantries
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True, blank=True)
def __str__(self):
return self.name
@ -78,17 +78,15 @@ class PantryItemLine(models.Model):
pantry_item = models.ForeignKey(PantryItem, on_delete=models.PROTECT)
quantity = models.IntegerField(default=1)
expiry_date = models.DateField(null=True, blank=True)
size = models.IntegerField(default=1)
size = models.IntegerField(default=1) #, decimal_places=3, max_digits=32)
info = models.CharField(max_length=200, null=True, blank=True)
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True, blank=True)
def unit(self):
return self.pantry_item.unit
def get_absolute_url(self):
return reverse('im:pantryitemlinedetail', kwargs={'pk': self.pk})
return reverse('pantryitemlinedetail', kwargs={'pk': self.pk})
def __str__(self):
return ' '.join([str(x) for x in [self.pantry_item.name, self.quantity, 'X',
self.size, self.pantry_item.unit]])
return ' '.join([str(x) for x in [self.pantry_item.name, self.quantity, 'X', self.size, self.pantry_item.unit]])

View file

@ -1,89 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Inventory Management</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">Inventory Management</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'im:consumelist' %} ">Consume</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'im:additemline' %} ">Add</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'im:shoppinglist' %} ">Shopping List</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'im:expirations' %} ">Expirations</a>
</li>
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'logout' %}">Logout {{ user.username }}</a>
<li class="nav-item">
<a class="nav-link" href="{% url 'password_change' %} ">Change Password</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %} ">Admin</a>
</li>
{% else %}
<a class="nav-link" href="{% url 'login' %}">Login</a>
{% endif %}
</li>
<!--li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Dropdown
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li!-->
</ul>
<form class="form-inline my-2 my-lg-0" action="{% url 'admin:inventory_pantryitem_changelist' %}" method="GET">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="q">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
<div class="container">
<div class="row justify-content-center">
<div class="col-8">
{% block content %}
BASE TEMPLATE
{% endblock %}
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
</body>
</html>

View file

@ -4,13 +4,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<title>Inventory Management</title>
<title>Pantry Inventory</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">Inventory Management</a>
<a class="navbar-brand" href="/">Pantry inventory</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@ -18,10 +18,10 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'im:consumelist' %} ">Consume</a>
<a class="nav-link" href="{% url 'im:consume' %} ">Consume</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'im:additemline' %} ">Add</a>
<a class="nav-link" href="{% url 'admin:inventory_pantryitem_changelist' %} ">Add</a>
</li>
<li class="nav-item">
@ -31,22 +31,8 @@
<a class="nav-link" href="{% url 'im:expirations' %} ">Expirations</a>
</li>
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'logout' %}">Logout {{ user.username }}</a>
<li class="nav-item">
<a class="nav-link" href="{% url 'password_change' %} ">Change Password</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'admin:index' %} ">Admin</a>
</li>
{% else %}
<a class="nav-link" href="{% url 'login' %}">Login</a>
{% endif %}
</li>
</li>
<!--li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

View file

@ -1,12 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h2>Add Category</h2>
<form action="." method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
{% endblock %}

View file

@ -1,10 +1,5 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Consume</h1>
Change quantity for "{{ object }}"
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Update">
</form>
TODO: add form to mark items as consumed
{% endblock %}

View file

@ -1,21 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Consume</h1>
{% if pis %}
{% regroup pis by expiry_date as pis_by_date %}
{% for date in pis_by_date %}
<div class="content">
<h2>{{ date.grouper|date:"d F Y" }}</h2>
<ul>
{% for pi in date.list %}
<li><a href="{% url 'im:consume' pi.id %}">{{ pi|title}}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% else %}
<p>No Pantry Items are available.</p>
{% endif %}
{% endblock %}

View file

@ -17,16 +17,5 @@ Expirations: See items that will expire soon
</p>
<p>
TODO: add items to your shopping list below
</p>
<p>
TODO: see expirted items below?
</p>
<p>
TODO: when no categories are found, show getting started, otherwise nit.
You will probably want to add some default Categories and Units
run TODO: https://pypi.org/project/django-smuggler/
</p>
{% endblock %}

View file

@ -1,12 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h2>Add Location</h2>
<form action="." method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
If you can not find the Category you want to add a new Line item from, add a new one <a href="../addcategory/">here</a>
<h2>Add Item Line</h2>
<form action="/additemline/" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
{% endblock %}

View file

@ -1,6 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Item Details</h1>
{{ object }}
{% endblock %}

View file

@ -1,17 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<p>
If you can not find the Pantry Item you want to add a new Line item from, add a new one <a href="../additem/">here</a>
</p>
<p>
If you can not find the Location you want to add a new Line item from, add a new one <a href="../addlocation/">here</a>
</p>
<h2>Add Item Line</h2>
<form action="/additemline/" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
{% endblock %}

View file

@ -1,10 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h2>Units</h2>
<ul>
{% for unit in object_list %}
<li>{{ unit.name }}</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1,25 +1,3 @@
from django.test import TestCase
from django.test import Client
from inventory.models import PantryItem, Category
class PantryItemTestCase(TestCase):
""" simple test case for a model"""
def setUp(self):
cat = Category.objects.create(name="UNCATEGORIZED")
PantryItem.objects.create(name="testitem", category=cat)
def test_pantryitem_looksok(self):
"""Pantryitems to string is ok"""
testitem = PantryItem.objects.get(name="testitem")
self.assertEqual(str(testitem), 'testitem')
class InterFaceTestCase(TestCase):
"""Simple test case for the web interface"""
def test_consume_view_exists(self):
client = Client()
response = client.get('/consume/')
self.assertTrue(b"TODO" not in response.content)
# Create your tests here.

View file

@ -4,14 +4,10 @@ from . import views
app_name = 'im'
urlpatterns = [
path('', views.index, name='index'),
path('items/<pk>', views.PantryItemLineView.as_view(), name='pantryitemlinedetail'),
path('consume/', views.ConsumeList.as_view(), name='consumelist'),
path('consume/<pk>/', views.Consume.as_view(), name='consume'),
path('consume/', views.consume, name='consume'),
path('shoppinglist/', views.Shoppinglist.as_view(), name='shoppinglist'),
# TODO: add exiperes before X date?
# TODO: add categories
# TODO: add pantry item selection
path('expirations/', views.Expirations.as_view(), name='expirations'),
path('units/', views.UnitView.as_view(), name='units'),
path('additemline/', views.PantryItemLineCreateView.as_view(), name='additemline'),
path('additem/', views.PantryItemCreateView.as_view(), name='additem'),
path('addcategory/', views.CategoryCreateView.as_view(), name='addcategory'),
path('addlocation/', views.LocationCreateView.as_view(), name='addlocation'),
]

View file

@ -1,45 +1,11 @@
from django.shortcuts import render
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.views import generic
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.db.models import F, Sum, Q
from django.views.generic import ListView
from inventory.models import Unit, Location
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from .models import PantryItem, PantryItemLine, Category
class PantryItemLineCreateView(LoginRequiredMixin, CreateView):
model = PantryItemLine
fields = '__all__'
success_url = '.'
class PantryItemCreateView(LoginRequiredMixin, CreateView):
model = PantryItem
fields = '__all__'
success_url = '.'
class CategoryCreateView(LoginRequiredMixin, CreateView):
model = Category
fields = '__all__'
success_url = '.'
class LocationCreateView(LoginRequiredMixin, CreateView):
model = Location
fields = '__all__'
success_url = '.'
class UnitView(ListView):
model = Unit
from .models import PantryItem, PantryItemLine
# Create your views here.
@ -48,6 +14,10 @@ def index(request):
return render(request, "inventory/index.html")
def consume(request):
return render(request, "inventory/consume.html")
class Shoppinglist(generic.ListView):
template_name = "inventory/shoppinglist.html"
context_object_name = 'pis'
@ -56,10 +26,9 @@ class Shoppinglist(generic.ListView):
"""
Return pantryitems for which we have None itemlines or Itemlines whith a total quantitly less than required
"""
return PantryItem.objects.annotate(
total_quantity=Sum(F('pantryitemline__quantity') *
F('pantryitemline__size'))
).filter(Q(min_quantity__gt=F('total_quantity')) | Q(pantryitemline=None, min_quantity__gt=0))
return PantryItem.objects.annotate(total_quantity=Sum(F('pantryitemline__quantity') *
F('pantryitemline__size'))).filter(Q(min_quantity__gt=F('total_quantity')) | Q(pantryitemline=None,
min_quantity__gt=0))
class Expirations(generic.ListView):
@ -68,25 +37,3 @@ class Expirations(generic.ListView):
def get_queryset(self):
return PantryItemLine.objects.exclude(expiry_date__isnull=True).exclude(quantity=0).order_by('expiry_date')
class ConsumeList(generic.ListView):
template_name = "inventory/consumelist.html"
context_object_name = 'pis'
def get_queryset(self):
return PantryItemLine.objects.exclude(quantity=0).order_by('expiry_date')
class Consume(generic.UpdateView):
template_name = "inventory/consume.html"
fields = ['quantity']
context_object_name = 'pis'
def get_queryset(self):
return PantryItemLine.objects.exclude(quantity=0).order_by('expiry_date')
class PantryItemLineView(generic.DetailView):
model = PantryItemLine

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
import os
import sys

View file

@ -1,3 +1,3 @@
#requirements.txt
django>=2.2.19
django>=2.2.8
gunicorn

View file

@ -1,9 +0,0 @@
[flake8]
max-line-length = 120
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[coverage:run]
include = '.'
omit = *migrations*, *tests*
plugins =
django_coverage_plugin

View file

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

View file

@ -1,5 +0,0 @@
from django.contrib.auth.forms import UserCreationForm
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
fields = UserCreationForm.Meta.fields + ("email",)

View file

@ -1,15 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h2>Login</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Login">
</form>
<a href="{% url 'im:index' %}">Back to index</a>
<a href="{% url 'register' %}">Register</a>
<a href="{% url 'password_reset' %}">Reset Password</a>
{% endblock %}

View file

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>Password changed</h2>
<a href="{% url 'im:index' %}">Back to index</a>
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h2>Change Password</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Change">
</form>
<a href="{% url 'im:index' %}">Back to index</a>
{% endblock %}

View file

@ -1,8 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>Password reset complete</h2>
<a href="{% url 'login' %}">Back to login</a>
{% endblock %}

View file

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>Confirm password reset</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Confirm">
</form>
{% endblock %}

View file

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>Password reset done</h2>
<a href="{% url 'login' %}">Back to login</a>
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>Send password reset link</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Reset">
</form>
<a href="{% url 'im:index' %}">Back to index</a>
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h2>Register</h2>
<form method="post">
{% csrf_token %}
{{form}}
<input type="submit" value="Register">
</form>
<a href="{% url 'login' %}">Back to login</a>
{% endblock %}

View file

@ -1,37 +0,0 @@
from django.contrib.auth import login
from django.shortcuts import redirect, render
from django.urls import reverse
from users.forms import CustomUserCreationForm
def dashboard(request):
return render(request, "users/dashboard.html")
def register(request):
if request.method == "GET":
return render(
request, "users/register.html",
{"form": CustomUserCreationForm}
)
elif request.method == "POST":
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect(reverse("index"))