1
1

copy whole project from another repo

This commit is contained in:
2020-02-18 16:01:28 +01:00
commit aadff671bd
30 changed files with 1290 additions and 0 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.dockerignore
.gitignore
docker-compose.yml
Dockerfile
volumes

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
**/__pycache__
volumes

7
Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM python:3.8.1
ENV PYTHONUNBUFFERED=1
WORKDIR /code/
COPY requirements.txt .
RUN pip install --no-cache-dir --requirement requirements.txt
COPY . .
ENTRYPOINT ["./entrypoint.sh"]

32
docker-compose.yml Normal file
View File

@@ -0,0 +1,32 @@
version: "3.7"
services:
db:
image: postgres:12.2-alpine
environment:
POSTGRES_DB: "floplanner_db"
POSTGRES_USER: "floplanner_user"
POSTGRES_PASSWORD: "hhktinWzk1SRg7K6eW0e45hUNLn8ZU"
expose:
- "5432"
volumes:
- ./volumes/db/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
web:
build:
context: .
environment:
DB_HOST: "db"
DB_PORT: "5432"
DB_NAME: "floplanner_db"
DB_USER: "floplanner_user"
DB_PASSWORD: "hhktinWzk1SRg7K6eW0e45hUNLn8ZU"
ports:
- "80:8000"
links:
- db
volumes:
- .:/code
- /etc/localtime:/etc/localtime:ro

14
entrypoint.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
if [ "$DJANGO_PRODUCTION_MODE" == "true" ]; then
echo "starting production server ..."
./wait-for-it.sh --host=${DB_HOST} --port=${DB_PORT} --strict --timeout=20 -- \
uwsgi --ini uwsgi.ini
else
echo "starting development server ..."
./wait-for-it.sh --host=${DB_HOST} --port=${DB_PORT} --strict --timeout=20 -- \
python manage.py runserver 0.0.0.0:8000
fi

View File

60
financeplanner/admin.py Normal file
View File

@@ -0,0 +1,60 @@
from django.contrib import admin
from django.urls import reverse_lazy
from django.utils.safestring import mark_safe
from financeplanner.models import Transaction, Balance
from financeplanner.utils import format_price, get_transaction_progress
class AdminSite(admin.AdminSite):
index_title = "FloPlanner"
site_title = "Admin Panel"
site_header = "Admin Panel"
site_url = reverse_lazy("finance:index")
admin_site = AdminSite()
def amount(obj):
return format_price(obj.amount)
amount.short_description = "amount"
def progress(transaction):
total, paid = get_transaction_progress(transaction)
if total:
percentage = int((paid / total) * 100)
else:
percentage = 0
outer_style = (
"width: 90%;",
"height: 10px;",
"background-color: #dd4646;",
)
inner_style = (
f"width: {percentage}%;",
"height: 100%;",
"background-color: #70bf2b;",
)
return mark_safe(
f'<div style="{" ".join(outer_style)}">'
f'<div style="{" ".join(inner_style)}"></div>'
f'</div>'
)
progress.short_description = "progress"
@admin.register(Transaction, site=admin_site)
class TransactionAdmin(admin.ModelAdmin):
list_display = ("subject", amount, "booking_date", "recurring_months", "not_recurring_after", progress)
@admin.register(Balance, site=admin_site)
class BalanceAdmin(admin.ModelAdmin):
list_display = ("date", amount)
ordering = ("-date",)

6
financeplanner/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class FinancePlannerConfig(AppConfig):
name = "financeplanner"
verbose_name = "FinancePlanner"

View File

View File

@@ -0,0 +1,20 @@
from django.contrib.auth.management.commands import createsuperuser
from django.db import transaction
USERNAME = "flo"
class Command(createsuperuser.Command):
help = "Used to create the superuser 'flo' with password '123'"
@transaction.atomic()
def handle(self, *args, **options):
options.update({
self.UserModel.USERNAME_FIELD: USERNAME,
"email": "flo@test.com",
"interactive": False,
})
super().handle(*args, **options)
user = self.UserModel.objects.latest("pk")
user.set_password("123")
user.save()

View File

@@ -0,0 +1,67 @@
from datetime import datetime, timedelta
from decimal import Decimal
from django.contrib.auth import get_user_model
from django.core.management import BaseCommand
from financeplanner.management.commands.createadmin import USERNAME
from financeplanner.models import Transaction, Balance
User = get_user_model()
class Command(BaseCommand):
def handle(self, *args, **options):
user = User.objects.get(**{User.USERNAME_FIELD: USERNAME})
today = datetime.now().date()
Transaction.objects.create(
user=user,
subject="simple transaction some days ago",
amount=Decimal("34.95"),
booking_date=today - timedelta(days=5),
)
Transaction.objects.create(
user=user,
subject="simple transaction a week ahead",
amount=Decimal("66.66"),
booking_date=today + timedelta(weeks=1),
)
Transaction.objects.create(
user=user,
subject="recurring stuff ending soon",
amount=Decimal("49"),
booking_date=today - timedelta(days=180),
recurring_months=1,
not_recurring_after=today + timedelta(weeks=6)
)
Transaction.objects.create(
user=user,
subject="recurring stuff beginning soon",
amount=Decimal("30"),
recurring_months=2,
booking_date=today + timedelta(weeks=2),
)
Balance.objects.create(
user=user,
date=today - timedelta(weeks=7),
amount=Decimal("600"),
)
Balance.objects.create(
user=user,
date=today - timedelta(weeks=3),
amount=Decimal("380"),
)
Balance.objects.create(
user=user,
date=today - timedelta(days=2),
amount=Decimal("450"),
)

View File

@@ -0,0 +1,42 @@
# Generated by Django 3.0.3 on 2020-02-18 08:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import financeplanner.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=100, verbose_name='subject')),
('amount', financeplanner.models.PriceField(decimal_places=2, max_digits=8, verbose_name='amount')),
('booking_date', models.DateField(verbose_name='booking date')),
('recurring_months', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='recurring months')),
('not_recurring_after', models.DateField(blank=True, null=True, verbose_name='not recurring after')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Balance',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(verbose_name='date')),
('amount', financeplanner.models.PriceField(decimal_places=2, max_digits=8, verbose_name='amount')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='balances', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'date')},
},
),
]

View File

60
financeplanner/models.py Normal file
View File

@@ -0,0 +1,60 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
from financeplanner.utils import round_with_dec_places
User = get_user_model()
class PriceField(models.DecimalField):
def to_python(self, value):
value = super(PriceField, self).to_python(value)
return round_with_dec_places(value, self.decimal_places)
def __init__(self, *args, **kwargs):
# allowing numbers until 999,999.99
kwargs['max_digits'] = 8
kwargs['decimal_places'] = 2
super().__init__(*args, **kwargs)
class BaseModel(models.Model):
class Meta:
abstract = True
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
self.full_clean()
super().save(force_insert, force_update, using, update_fields)
class Transaction(models.Model):
user = models.ForeignKey(to=User, related_name="transactions", on_delete=models.CASCADE)
subject = models.CharField("subject", max_length=100)
amount = PriceField("amount")
booking_date = models.DateField("booking date")
recurring_months = models.PositiveSmallIntegerField("recurring months", null=True, blank=True)
not_recurring_after = models.DateField("not recurring after", null=True, blank=True)
def __str__(self):
return f"{self.subject}: {self.amount:.2f}"
def clean(self):
if self.recurring_months is not None and self.recurring_months < 1:
raise ValidationError("recurring_months must be at least 1, if present")
if self.not_recurring_after and self.not_recurring_after <= self.booking_date:
raise ValidationError("not_recurring_after must be later than booking_date")
class Balance(models.Model):
class Meta:
unique_together = ("user", "date")
user = models.ForeignKey(to=User, related_name="balances", on_delete=models.CASCADE)
date = models.DateField("date")
amount = PriceField("amount")
def __str__(self):
return f"{self.date}: {self.amount:.2f}"

View File

@@ -0,0 +1,212 @@
* {
color: #FFFCF9;
}
html, body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: none;
}
body {
display: flex;
flex-direction: column;
align-items: center;
background-color: #131211;
padding-bottom: 24px;
}
h1 {
margin-top: 18px;
margin-bottom: 6px;
font-size: 24px;
font-weight: normal;
color: #FFCC99;
}
a {
color: #FFCC99;
text-decoration: none;
transition: color 150ms;
}
a:hover, a:focus {
color: #CC6622;
}
.mini-link {
font-size: 14px;
}
.flex-col-centering {
display: flex;
flex-direction: column;
align-items: center;
}
.text-centering {
text-align: center;
}
.main-container {
flex-shrink: 1;
width: 90%;
max-width: 1200px;
padding: 12px;
margin-top: 12px;
background-color: #282624;
border-radius: 4px;
font-size: 16px;
}
.login-form {
flex: 1;
width: 95%;
max-width: 400px;
}
.login-line {
flex: 1;
width: 95%;
margin: 6px;
padding: 3px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.login-label {
flex-shrink: 1;
margin-right: 6px;
padding: 2px;
text-align: right;
}
.login-textbox {
flex-grow: 1;
padding: 2px;
min-width: 60px;
background-color: #DDDDDD;
color: #131211;
border: none;
border-radius: 4px;
}
.login-warning {
flex: 1;
padding: 2px;
text-align: center;
color: #CC6622;
}
.login-button {
flex: 0 0 120px;
padding: 4px;
background-color: #202020;
border: 1px solid #666666;
border-radius: 4px;
color: #FFCC99;
transition: color 150ms, background-color 150ms;
}
.login-button:hover, .login-button:focus {
background-color: #131211;
color: #CC6622;
}
#graph {
flex: 0 0 300px;
width: 100%;
display: flex;
flex-direction: row;
align-items: end;
padding-bottom: 6px;
overflow-x: scroll;
transition: opacity 100ms;
}
#graph.scrolling {
opacity: 75%;
cursor: grabbing;
}
.graph-bar {
flex: 1 1 0;
min-width: 8px;
min-height: 8px;
margin: 1px;
border-radius: 2px;
opacity: 60%;
}
.graph-bar.highlighted {
opacity: 100%;
border: 1px solid #FFFCF9;
}
.graph-bar.strong {
opacity: 80%;
}
.graph-bar.weak {
height: 40%;
opacity: 40%;
}
.graph-bar.green {
background-color: #55DD22;
}
.graph-bar.olive {
background-color: #99CC33;
}
.graph-bar.yellow {
background-color: #CCCC33;
}
.graph-bar.orange {
background-color: #CC9933;
}
.graph-bar.red {
background-color: #DD5522;
}
.graph-bar.grey {
background-color: #777766;
}
.graph-bar:hover {
background-color: #FFFCF9;
box-shadow: #FFFCF9 0 0 12px;
opacity: 100%;
}
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
opacity: 90%;
width: 150px;
min-height: 50px;
background-color: #131211;
padding: 6px;
border-radius: 4px;
position: absolute;
bottom: 100%;
margin-bottom: 3px;
left: 50%;
margin-left: -75px;
z-index: 1;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}

View File

@@ -0,0 +1,115 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FinancePlanner</title>
<link rel="stylesheet" type="text/css" href="{% static 'financeplanner/style.css' %}">
</head>
<body>
<h1>&#x1F4CA; FinancePlanner</h1>
<p>
<a href="{% url 'admin:index' %}" class="mini-link">Admin Panel</a>
-
<a href="{% url 'logout' %}" class="mini-link">Logout</a>
</p>
<div class="main-container flex-col-centering">
{% if graph_data %}
<div id="graph">
{% for date, stat in graph_data.items %}
{% if stat.div_percentage is None %}
<div class="graph-bar weak grey tooltip">
<span class="tooltiptext">{{ date }}<br>unknown</span>
</div>
{% else %}
<div class="graph-bar {{ stat.div_opacity_class }} {{ stat.div_color_class }} tooltip"
style="height: {{ stat.div_percentage }}%">
<span class="tooltiptext">{{ date }}<br>{{ stat.amount }}€</span>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<p>No graph data available.</p>
{% endif %}
</div>
<div class="main-container">
<p>Showing data from {{ range_start }} til {{ range_end }}:</p>
<p>Balances:</p>
{% if balance_list %}
<ul>
{% for balance in balance_list %}
<li>{{ balance }}</li>
{% endfor %}
</ul>
{% else %}
<p>No balances available.</p>
{% endif %}
<p>Transactions:</p>
{% if transaction_list %}
<ul>
{% for transaction in transaction_list %}
<li>{{ transaction }}</li>
{% endfor %}
</ul>
{% else %}
<p>No transactions available.</p>
{% endif %}
<p>Calculated Transactions:</p>
{% if actual_transactions %}
<ul>
{% for date, subject, amount in actual_transactions %}
<li>{{ date }} - {{ subject }} - {{ amount }}</li>
{% endfor %}
</ul>
{% else %}
<p>No calculated transactions available.</p>
{% endif %}
</div>
<script type="text/javascript">
const slider = document.getElementById('graph');
let scrolling = false;
let initX;
let initScrollLeft;
function startDragScroll(event) {
scrolling = true;
slider.classList.add('scrolling');
initX = event.pageX - slider.offsetLeft;
initScrollLeft = slider.scrollLeft;
}
function stopDragScroll() {
scrolling = false;
slider.classList.remove('scrolling');
}
function doDragScroll(event) {
if (scrolling) {
event.preventDefault();
const newX = event.pageX - slider.offsetLeft;
const diff = (newX - initX) * 1.5;
slider.scrollLeft = initScrollLeft - diff;
}
}
slider.addEventListener('mousedown', (event) => startDragScroll(event));
slider.addEventListener('mouseleave', () => stopDragScroll());
slider.addEventListener('mouseup', () => stopDragScroll());
slider.addEventListener('mousemove', (event) => doDragScroll(event));
</script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FloPlanner - Logout</title>
<link rel="stylesheet" type="text/css" href="{% static 'financeplanner/style.css' %}">
</head>
<body>
<h1>FloPlanner</h1>
<p>
<a href="{% url 'admin:index' %}" class="mini-link">Admin Panel</a>
-
<a href="{% url 'login' %}" class="mini-link">Login</a>
</p>
<div class="main-container text-centering">
<p>You have been logged out.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FloPlanner - Login</title>
<link rel="stylesheet" type="text/css" href="{% static 'financeplanner/style.css' %}">
</head>
<body>
<h1>FloPlanner</h1>
<p>
<a href="{% url 'admin:index' %}" class="mini-link">Admin Panel</a>
</p>
<div class="main-container flex-col-centering">
<form id="login-form" method="post" class="login-form" action="{% url 'login' %}">
{% csrf_token %}
<p class="login-line">
<label for="id_username" class="login-label">Username</label>
<input id="id_username" name="username" type="text" class="login-textbox">
</p>
<p class="login-line">
<label for="id_password" class="login-label">Password</label>
<input id="id_password" name="password" type="password" class="login-textbox">
</p>
{% if form.errors %}
<p class="login-line login-warning">Wrong credentials, please try again.</p>
{% endif %}
<p class="login-line">
<input type="submit" value="Login" class="login-button"/>
</p>
<input type="hidden" name="next" value="{{ next }}"/>
</form>
</div>
</body>
</html>

7
financeplanner/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import path
from financeplanner import views
urlpatterns = [
path("", views.index, name="index"),
]

57
financeplanner/utils.py Normal file
View File

@@ -0,0 +1,57 @@
from datetime import datetime
from decimal import Decimal
from dateutil.relativedelta import relativedelta
def current_date():
return datetime.now().date()
def format_date(d):
return d.strftime("%d.%m.%Y")
def round_with_dec_places(number, places):
if number is None:
return None
dec_num = number if isinstance(number, Decimal) else Decimal(number)
return dec_num.quantize(Decimal(10) ** -places)
def format_price(number):
if number is None:
return None
return f"{round_with_dec_places(number, 2)}"
def _count_rates(start, end, months_delta):
delta = relativedelta(months=months_delta)
count = 0
current = start
while current <= end:
count += 1
current += delta
return count
def get_transaction_progress(transaction):
if rec_months := transaction.recurring_months:
start = transaction.booking_date
current = current_date()
if end := transaction.not_recurring_after:
total = _count_rates(start, end, rec_months)
if current >= end:
paid = total
else:
paid = _count_rates(start, current, rec_months)
else:
total = None
paid = _count_rates(start, current, rec_months)
else:
total = 1
if transaction.booking_date < current_date():
paid = 1
else:
paid = 0
return total, paid

174
financeplanner/views.py Normal file
View File

@@ -0,0 +1,174 @@
from datetime import datetime
from decimal import Decimal
from math import floor
from dateutil.relativedelta import relativedelta
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.shortcuts import render
from financeplanner.utils import format_date
def _get_relevant_balances(user, start, end):
balances = user.balances.filter(date__range=(start, end)).order_by("date")
return list(balances)
def _get_relevant_transactions(user, start, end):
transactions = user.transactions.filter(
Q(booking_date__range=(start, end)) |
Q(booking_date__lte=end, recurring_months__isnull=False, not_recurring_after__gte=start)
).order_by("booking_date")
return list(transactions)
def _calculate_actual_transactions(start, end, transaction_list):
result = []
for trans in transaction_list:
current_calc_trans = {}
if trans.recurring_months:
current_date = trans.booking_date
step = 1
limit = min(trans.not_recurring_after, end) if trans.not_recurring_after else end
while current_date < limit:
if current_date >= start:
result.append((
current_date,
f"{trans.subject} #{step}",
trans.amount,
))
current_date += relativedelta(months=trans.recurring_months)
step += 1
else:
current_calc_trans[trans.booking_date] = trans.amount
result.append((
trans.booking_date,
trans.subject,
trans.amount,
))
return result
def _calculate_daily_stats(start, end, balance_list, actual_transactions):
stats = {}
current_date = start
current_amount = None
while current_date < end:
current_stats = {
"balance": None,
"transactions": [],
"amount": None,
}
relevant_balances = [bal for bal in balance_list if bal.date == current_date]
if relevant_balances:
assert len(relevant_balances) == 1, f"balances should be unique for user and date, " \
f"but here are {relevant_balances}"
amount = relevant_balances[0].amount
current_stats["balance"] = relevant_balances[0].amount
current_amount = amount
relevant_transactions = [tra for tra in actual_transactions if tra[0] == current_date]
for tra in relevant_transactions:
subject, amount = tra[1], tra[2]
current_stats["transactions"].append({
"subject": subject,
"amount": amount,
})
if current_amount is not None:
current_amount -= amount
current_stats["amount"] = current_amount
stats[current_date] = current_stats
current_date += relativedelta(days=1)
return stats
def _floor_to_first_two_places(dec):
if dec < 100:
return 100
exp = floor(dec.log10()) - 1
factor = 10 ** exp
return factor * int(dec / factor)
def _calculate_scale(stats):
max_amount = max(stat["amount"] for stat in stats.values() if stat["amount"] is not None)
return _floor_to_first_two_places(max_amount * Decimal("1.3"))
def _build_graph_data(start, end, balance_list, actual_transactions):
"""
result has the format:
{
<date>: {
"balance": <decimal>,
"transactions": [
{
"subject": <str>,
"amount": <decimal>,
},
...
],
"amount": <decimal>,
"div_percentage": <int>,
"div_opacity_class": <str>,
"div_color_class": <str>,
},
...
}
"""
today = datetime.now().date()
stats = _calculate_daily_stats(start, end, balance_list, actual_transactions)
amount_limit = _calculate_scale(stats)
for date in stats.keys():
amount = stats[date]["amount"]
if amount is None:
stats[date]["div_percentage"] = None
else:
percentage = int((amount / amount_limit) * 100)
stats[date]["div_percentage"] = percentage
if percentage >= 60:
stats[date]["div_color_class"] = "green"
elif percentage >= 45:
stats[date]["div_color_class"] = "olive"
elif percentage >= 30:
stats[date]["div_color_class"] = "yellow"
elif percentage >= 15:
stats[date]["div_color_class"] = "orange"
else:
stats[date]["div_color_class"] = "red"
if date == today:
stats[date]["div_opacity_class"] = "highlighted"
elif stats[date]["balance"] or stats[date]["transactions"]:
stats[date]["div_opacity_class"] = "strong"
else:
stats[date]["div_opacity_class"] = ""
return stats
@login_required
def index(request):
user = request.user
today = datetime.now().date()
range_start = today - relativedelta(days=30)
range_end = today + relativedelta(days=50)
balance_list = _get_relevant_balances(user, range_start, range_end)
transaction_list = _get_relevant_transactions(user, range_start, range_end)
actual_transactions = _calculate_actual_transactions(range_start, range_end, transaction_list)
graph_data = _build_graph_data(range_start, range_end, balance_list, actual_transactions)
context = {
"range_start": format_date(range_start),
"range_end": format_date(range_end),
"today": today,
"balance_list": balance_list,
"transaction_list": transaction_list,
"actual_transactions": actual_transactions,
"graph_data": graph_data,
}
return render(request, "financeplanner/index.html", context)

0
floplanner/__init__.py Normal file
View File

106
floplanner/settings.py Normal file
View File

@@ -0,0 +1,106 @@
import os
from django.urls import reverse_lazy
def _get_env_production_mode():
env_var = os.environ.get("DJANGO_PRODUCTION_MODE", "false")
return env_var.lower() == "true"
def _get_env_secret_key():
env_var = os.environ.get("DJANGO_SECRET_KEY")
assert env_var is not None, "DJANGO_SECRET_KEY environment variable must be set when using production mode"
assert 30 <= len(env_var) <= 80, "DJANGO_SECRET_KEY should be 30 to 80 characters long"
return env_var
def _get_env_allowed_hosts():
env_var = os.environ.get("DJANGO_ALLOWED_HOSTS")
assert env_var is not None, "DJANGO_ALLOWED_HOSTS environment variable must be set when using production mode"
return [host.strip() for host in env_var.split(",")]
def _get_env_static_root():
env_var = os.environ.get("DJANGO_STATIC_ROOT")
assert env_var is not None, "DJANGO_STATIC_ROOT environment variable must be set when using production mode"
return env_var
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PRODUCTION = _get_env_production_mode()
if PRODUCTION:
SECRET_KEY = _get_env_secret_key()
DEBUG = False
ALLOWED_HOSTS = _get_env_allowed_hosts()
STATIC_ROOT = _get_env_static_root()
else:
SECRET_KEY = "QetNMYdSKo3kefmltcEeAu52HbyZBxXsROiYesIEwYwnX0rCuv"
DEBUG = True
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = [
"financeplanner",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "floplanner.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "floplanner.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"HOST": os.environ.get("DB_HOST"),
"PORT": os.environ.get("DB_PORT"),
"NAME": os.environ.get("DB_NAME"),
"USER": os.environ.get("DB_USER"),
"PASSWORD": os.environ.get("DB_PASSWORD"),
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", },
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", },
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", },
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", },
]
LOGIN_URL = reverse_lazy("login")
LOGIN_REDIRECT_URL = reverse_lazy("finance:index")
STATIC_URL = "/static/"

13
floplanner/urls.py Normal file
View File

@@ -0,0 +1,13 @@
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path, include
from django.views.generic import RedirectView
from financeplanner.admin import admin_site
urlpatterns = [
path("admin/", admin_site.urls),
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("finance/", include(("financeplanner.urls", "financeplanner"), namespace="finance")),
path("", RedirectView.as_view(pattern_name="finance:index", permanent=False), name="index"),
]

16
floplanner/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for floplanner project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'floplanner.settings')
application = get_wsgi_application()

21
manage.py Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'floplanner.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
django==3.0.3
psycopg2-binary==2.8.4
uwsgi==2.0.18
python-dateutil==2.8.1

6
uwsgi.ini Normal file
View File

@@ -0,0 +1,6 @@
[uwsgi]
chdir = /code
module = floplanner.wsgi:application
http = 0.0.0.0:8000
workers = 2
master = true

178
wait-for-it.sh Executable file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
WAITFORIT_BUSYTIMEFLAG="-t"
else
WAITFORIT_ISBUSY=0
WAITFORIT_BUSYTIMEFLAG=""
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi