copy whole project from another repo
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.dockerignore
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
volumes
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
**/__pycache__
|
||||
volumes
|
||||
7
Dockerfile
Normal file
7
Dockerfile
Normal 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
32
docker-compose.yml
Normal 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
14
entrypoint.sh
Executable 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
|
||||
0
financeplanner/__init__.py
Normal file
0
financeplanner/__init__.py
Normal file
60
financeplanner/admin.py
Normal file
60
financeplanner/admin.py
Normal 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
6
financeplanner/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FinancePlannerConfig(AppConfig):
|
||||
name = "financeplanner"
|
||||
verbose_name = "FinancePlanner"
|
||||
0
financeplanner/management/__init__.py
Normal file
0
financeplanner/management/__init__.py
Normal file
0
financeplanner/management/commands/__init__.py
Normal file
0
financeplanner/management/commands/__init__.py
Normal file
20
financeplanner/management/commands/createadmin.py
Normal file
20
financeplanner/management/commands/createadmin.py
Normal 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()
|
||||
67
financeplanner/management/commands/filldummydata.py
Normal file
67
financeplanner/management/commands/filldummydata.py
Normal 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"),
|
||||
)
|
||||
42
financeplanner/migrations/0001_initial.py
Normal file
42
financeplanner/migrations/0001_initial.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
financeplanner/migrations/__init__.py
Normal file
0
financeplanner/migrations/__init__.py
Normal file
60
financeplanner/models.py
Normal file
60
financeplanner/models.py
Normal 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}€"
|
||||
212
financeplanner/static/financeplanner/style.css
Normal file
212
financeplanner/static/financeplanner/style.css
Normal 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;
|
||||
}
|
||||
115
financeplanner/templates/financeplanner/index.html
Normal file
115
financeplanner/templates/financeplanner/index.html
Normal 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>📊 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>
|
||||
25
financeplanner/templates/registration/logged_out.html
Normal file
25
financeplanner/templates/registration/logged_out.html
Normal 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>
|
||||
40
financeplanner/templates/registration/login.html
Normal file
40
financeplanner/templates/registration/login.html
Normal 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
7
financeplanner/urls.py
Normal 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
57
financeplanner/utils.py
Normal 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
174
financeplanner/views.py
Normal 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
0
floplanner/__init__.py
Normal file
106
floplanner/settings.py
Normal file
106
floplanner/settings.py
Normal 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
13
floplanner/urls.py
Normal 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
16
floplanner/wsgi.py
Normal 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
21
manage.py
Executable 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
4
requirements.txt
Normal 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
6
uwsgi.ini
Normal 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
178
wait-for-it.sh
Executable 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
|
||||
Reference in New Issue
Block a user