This commit is contained in:
GPP-USN 2019-09-23 09:20:49 +02:00
commit 0550c9cc19
32 changed files with 1424 additions and 183 deletions

17
BABEL_USAGE.md Normal file
View File

@ -0,0 +1,17 @@
# Babel usage
Babel will scan and detect sections of code that uses `gettext` or `_` in HTML templates and python code. It takes the constant string passed into its function and makes a translation template which can then be used to build individual translations.
## Usage
If you are compiling and building a template from scratch
```
$ pybabel extract -F babel.cfg -o messages.pot ./
$ pybabel update -i messages.pot -d translations
```
Make translations in the outputted file then compile the translation with
```
$ pybabel compile -d translations
```
Make sure the `messages.po` file is in the right language directory.
Example path: `translations/nb/LC_MESSAGES/messages.po`

View File

@ -9,7 +9,11 @@ Install the required dependencies with pip
``` ```
$ pip install -r requirements.txt $ pip install -r requirements.txt
``` ```
then, run the web server Make sure you compile the babel languages
```
$ pybabel compile -d translations
```
Lastly, run the web server
``` ```
$ python3 main.py $ python3 main.py
``` ```

3
babel.cfg Normal file
View File

@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@ -11,5 +11,12 @@
"auto_pull_and_restart": false, "auto_pull_and_restart": false,
"webhook_endpoint": "/api/git_commit", "webhook_endpoint": "/api/git_commit",
"secret": "iOnlyHavePullAccess" "secret": "iOnlyHavePullAccess"
},
"mysql": {
"host": "localhost",
"port": 3306,
"user": "husstanden",
"passwd": "",
"db": "db_husstanden"
} }
} }

View File

@ -1,8 +0,0 @@
from flask import render_template
from objects import glob
"""
@glob.app.template_filter('load_module')
def load_module(module_name):
return render_template("modules/%s.html" % module_name)
"""

122
forms/login.py Normal file
View File

@ -0,0 +1,122 @@
from wtforms import Form, BooleanField, StringField, PasswordField, TextAreaField, validators
from wtforms.fields.html5 import DateField, DecimalField, IntegerField, EmailField
from wtforms.widgets import TextArea
from flask_login import UserMixin
from flask_babel import gettext as _
from objects import glob
FORM_RENDER_KW = {
"class_": "form-control"
}
class BillForm(Form):
payment_to = StringField(_("Payment to"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
description = TextAreaField(_("Description"), render_kw = {
"cols": 55,
"rows": 8,
**FORM_RENDER_KW
})
sum = DecimalField(_("Sum"), render_kw = FORM_RENDER_KW)
kid = IntegerField(_("KID"), render_kw = FORM_RENDER_KW)
date_due = DateField(_("Date due"), render_kw = FORM_RENDER_KW)
class WarrantyForm(Form):
item = StringField(_("Item"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
date_from = DateField(_("Date of purchase"), render_kw = FORM_RENDER_KW)
date_to = DateField(_("Warranty duration"), render_kw = FORM_RENDER_KW)
sum = DecimalField(_("Sum"), render_kw = FORM_RENDER_KW)
class ServiceForm(Form):
name = StringField(_("Name"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
type = StringField(_("Type"), [validators.DataRequired()], render_kw = FORM_RENDER_KW)
contact = StringField(_("Contact"), render_kw = FORM_RENDER_KW)
phone = IntegerField(_("Phone"), render_kw = FORM_RENDER_KW)
website = StringField(_("Website"), render_kw = FORM_RENDER_KW)
class LoginForm(Form):
email = EmailField(_("Email"), [
validators.DataRequired(),
validators.Length(min=6, max=254)
],
render_kw = FORM_RENDER_KW)
password = PasswordField(_("Password"), [
validators.DataRequired(),
validators.Length(min=4, max=127)
],
render_kw = FORM_RENDER_KW)
class RegisterForm(Form):
email = EmailField(_("Email"), [
validators.DataRequired(),
validators.Length(min=6, max=254)
],
render_kw = FORM_RENDER_KW)
password = PasswordField(_("Password"), [
validators.DataRequired(),
validators.Length(min=4, max=127),
validators.EqualTo("confirm_password", message = _("Passwords must match"))
],
render_kw = FORM_RENDER_KW)
confirm_password = PasswordField(_("Repeat Password"), render_kw = FORM_RENDER_KW)
firstname = StringField(_("Firstname"), [
validators.DataRequired(),
validators.Length(min=2, max=30)
],
render_kw = FORM_RENDER_KW)
surname = StringField(_("Surname"), [
validators.DataRequired(),
validators.Length(min=2, max=30)
],
render_kw = FORM_RENDER_KW)
accept_tos = BooleanField(_("I accept the TOS"), [validators.DataRequired()])
class User(UserMixin):
id = -1
email = ""
password = ""
firstname = ""
surname = ""
def __init__(self, login):
self.fetch_from_db(login)
def fetch_from_db(self, login):
conn = glob.get_sql_connection()
cur = conn.cursor()
cur.execute("""
SELECT *
FROM Bruker
WHERE Epost = %s
LIMIT 1;
""", (login[0],))
user = cur.fetchone()
cur.close()
if user is None:
raise Exception(_("Invalid login"))
if not glob.check_password(login[1], user[2]):
raise Exception(_("Incorrect password"))
self.id, self.email, self.password, self.firstname, self.surname = user
def register_account(email, password, firstname, surname):
conn = glob.get_sql_connection()
cur = conn.cursor()
cur.execute("""
INSERT INTO
Bruker (Epost, Passord, Fornavn, Etternavn)
VALUES (%s, %s, %s, %s);
""", (email, glob.hash_password(password), firstname, surname))
conn.commit()
cur.close()

View File

@ -5101,7 +5101,7 @@ CREATE TABLE Husrelasjoner
( (
BrukerID INT(11), BrukerID INT(11),
HusID INT(11), HusID INT(11),
Privleges INT(11), Privileges INT(11),
PRIMARY KEY (BrukerID, HusID), PRIMARY KEY (BrukerID, HusID),
CONSTRAINT FOREIGN KEY (BrukerID) REFERENCES Bruker(BrukerID), CONSTRAINT FOREIGN KEY (BrukerID) REFERENCES Bruker(BrukerID),
CONSTRAINT FOREIGN KEY (HusID) REFERENCES Husstand(HusID) CONSTRAINT FOREIGN KEY (HusID) REFERENCES Husstand(HusID)

20
localizer.py Normal file
View File

@ -0,0 +1,20 @@
from flask import g, request, session
from flask_babel import Babel
from objects import glob
babel = Babel(glob.app)
LANGUAGES = {
"en": "English",
"no": "Norwegian"
}
@babel.localeselector
def get_locale():
# force session lang to be set
session["lang"] = session.get("lang", "en")
if request.args.get("lang"):
session["lang"] = request.args.get("lang") if request.args.get("lang") in LANGUAGES.keys() else "en"
return session.get("lang", "en")

View File

@ -3,9 +3,11 @@ from flask import Flask, url_for, request
from objects import glob # Global sharing of python objects in a manageable way from objects import glob # Global sharing of python objects in a manageable way
glob.app = Flask(__name__) glob.app = Flask(__name__)
glob.app.secret_key = "E2FGrJXLtOxPh70Q"
import localizer # Initialize localization (Babel)
import routes # All flask app routes import routes # All flask app routes
import filters # All flask app filters # import filters # All flask app filters
if glob.config["git"]["auto_pull_and_restart"]: # Only used on the VPS (Do not enable in config) if glob.config["git"]["auto_pull_and_restart"]: # Only used on the VPS (Do not enable in config)
@glob.app.route(glob.config["git"]["webhook_endpoint"], methods = ["POST"]) @glob.app.route(glob.config["git"]["webhook_endpoint"], methods = ["POST"])

View File

@ -1,11 +1,14 @@
import os import os
import json import json
import shutil import shutil
import mysql.connector
import bcrypt
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Global variables that is None by default and gets overwritten in other modules # Global variables that is None by default and gets overwritten in other modules
app = None # main.py -> Flask App app = None # main.py -> Flask App
sql_conn = None
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Global variables that initializes on first load of module # Global variables that initializes on first load of module
@ -16,3 +19,18 @@ if not os.path.isfile("config.json"):
with open("config.json", "r") as f: with open("config.json", "r") as f:
config = json.load(f) config = json.load(f)
def make_sql_connection():
return mysql.connector.connect(**config["mysql"])
def get_sql_connection():
global sql_conn
if sql_conn is None or not sql_conn.is_connected():
sql_conn = make_sql_connection()
return sql_conn
def hash_password(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(10, prefix=b"2a")).decode()
def check_password(p1, p2):
return bcrypt.checkpw(p1.encode(), p2.encode())

View File

@ -1,2 +1,6 @@
Flask>=1.0.2 Flask>=1.0.2
Flask-WTF>=0.14.2 Flask-WTF>=0.14.2
flask_login>=0.4.1
Flask-Babel>=0.12.2
mysql-connector
bcrypt

174
routes.py
View File

@ -1,16 +1,180 @@
from flask import render_template, url_for, request from flask import render_template, url_for, request, redirect, flash, abort
from wtforms import Form, BooleanField, StringField, PasswordField, validators
import flask_login
from forms.login import LoginForm, RegisterForm, BillForm, WarrantyForm, ServiceForm, User, register_account
from objects import glob # Global sharing of python objects in a manageable way from objects import glob # Global sharing of python objects in a manageable way
from flask_babel import gettext
login_manager = flask_login.LoginManager()
login_manager.init_app(glob.app)
login_manager.login_view = "login"
logged_in_users = []
@glob.app.route("/") @glob.app.route("/")
@glob.app.route("/home") @glob.app.route("/home")
@glob.app.route("/dashboard") @glob.app.route("/dashboard")
def home(): @flask_login.login_required
def dashboard():
return render_template("pages/dashboard.html") return render_template("pages/dashboard.html")
@glob.app.route("/bills", methods = ["GET", "POST"])
@flask_login.login_required
def bills():
form = BillForm(request.form)
conn = glob.make_sql_connection()
cur = conn.cursor()
if request.method == "POST" and form.validate():
cur.execute("""
INSERT
INTO Regninger
VALUES (NULL, %s, %s, %s, %s, %s, 0, 1, %s)
""", (form.payment_to.data, form.description.data, form.kid.data, form.sum.data, form.date_due.data, flask_login.current_user.id))
conn.commit()
return redirect(url_for("bills"))
cur.execute("""
SELECT Betaletil, Regningfor, Regningsum, KID, Betalingsfrist, Betalt
FROM Regninger
WHERE BrukerID = %s
""", (flask_login.current_user.id,))
data = []
for row in cur:
data.append(row)
cur.close()
conn.close()
return render_template("pages/bills.html", data=data, form=form)
@glob.app.route("/warranties", methods = ["GET", "POST"])
@flask_login.login_required
def warranties():
form = WarrantyForm(request.form)
conn = glob.make_sql_connection()
cur = conn.cursor()
if request.method == "POST" and form.validate():
cur.execute("""
INSERT
INTO Garanti
VALUES (NULL, %s, %s, %s, %s, 1, %s)
""", (form.item.data, form.date_from.data, form.date_to.data, form.sum.data, flask_login.current_user.id))
conn.commit()
return redirect(url_for("warranties"))
cur.execute("""
SELECT Vare, Kjøpsdato, Garantitil, Pris
FROM Garanti
WHERE BrukerID = %s
""", (flask_login.current_user.id,))
data = []
for row in cur:
data.append(row)
cur.close()
conn.close()
return render_template("pages/warranties.html", data=data, form=form)
@glob.app.route("/receipts", methods = ["GET", "POST"])
@flask_login.login_required
def receipts():
return render_template("pages/receipts.html")
@glob.app.route("/services", methods = ["GET", "POST"])
@flask_login.login_required
def services():
form = ServiceForm(request.form)
conn = glob.make_sql_connection()
cur = conn.cursor()
if request.method == "POST" and form.validate():
cur.execute("""
INSERT
INTO Services
VALUES (NULL, %s, %s, %s, %s, 1, %s, %s)
""", (form.name.data, form.type.data, form.contact.data, form.phone.data, flask_login.current_user.id, form.website.data))
conn.commit()
return redirect(url_for("services"))
cur.execute("""
SELECT ServiceName, ServiceType, Kontaktperson, Telefonnummer, Hjemmeside
FROM Services
WHERE BrukerID = %s
""", (flask_login.current_user.id,))
data = []
for row in cur:
data.append(row)
cur.close()
conn.close()
return render_template("pages/services.html", data=data, form=form)
@glob.app.route("/login", methods = ["GET", "POST"]) @glob.app.route("/login", methods = ["GET", "POST"])
def serve_login(): def login():
if flask_login.current_user.is_authenticated:
flash(gettext("Already logged in"), "info")
return redirect(url_for("dashboard"))
form_login = LoginForm(request.form)
form_register = RegisterForm(request.form)
if request.method == "POST": if request.method == "POST":
return "TODO: Login handle", 501 if form_register.validate():
return render_template("login.html") try:
register_account(form_register.email.data, form_register.password.data, form_register.firstname.data, form_register.surname.data)
flash(gettext("User registered"), "success")
except Exception as e:
flash(gettext(str(e)), "danger")
return redirect(url_for("login"))
elif form_login.validate():
try:
user = User((form_login.email.data, form_login.password.data))
flask_login.login_user(user)
logged_in_users.append(user)
flash(gettext("Logged in"), "success")
except Exception as e:
flash(gettext(str(e)), "danger")
return redirect(url_for("login"))
return redirect(url_for("dashboard")) # Valid login > Redirect to dashboard as user is logged in
return render_template("login.html", form = {
"login": form_login,
"register": form_register
})
@glob.app.route("/logout")
@flask_login.login_required
def logout():
flask_login.logout_user()
flash(gettext("Logged out"), "success")
return redirect(url_for("login"))
@glob.app.errorhandler(401)
def unauthorized_handler_err():
flash(gettext("Login is required"), "danger")
unauthorized_handler()
@login_manager.user_loader
def load_user(uuid):
uuid = int(uuid)
lst = [x for x in logged_in_users if x.id == uuid]
return lst[0] if len(lst) > 0 else None
@login_manager.unauthorized_handler
def unauthorized_handler():
return redirect(url_for("login"))

View File

@ -120,7 +120,7 @@ html[xmlns] .clearfix {
.calendar .clndr .clndr-table .header-days { .calendar .clndr .clndr-table .header-days {
height: 30px; height: 30px;
font-size: 10px; font-size: 10px;
background: #0D70A6; background: #506EE4;
} }
.calendar .clndr .clndr-table .header-days .header-day { .calendar .clndr .clndr-table .header-days .header-day {
vertical-align: middle; vertical-align: middle;
@ -139,8 +139,7 @@ html[xmlns] .clearfix {
vertical-align: top; vertical-align: top;
} }
.calendar .clndr .clndr-table tr .day { .calendar .clndr .clndr-table tr .day {
border-left: 1px solid #000000; border: 1px solid #000000;
border-top: 1px solid #000000;
width: 100%; width: 100%;
height: inherit; height: inherit;
} }
@ -149,23 +148,15 @@ html[xmlns] .clearfix {
} }
.calendar .clndr .clndr-table tr .day.today, .calendar .clndr .clndr-table tr .day.today,
.calendar .clndr .clndr-table tr .day.my-today { .calendar .clndr .clndr-table tr .day.my-today {
background: #9AD6E3; box-shadow: inset 0 0 0 2px black;
}
.calendar .clndr .clndr-table tr .day.today:hover,
.calendar .clndr .clndr-table tr .day.my-today:hover {
background: #72c6d8;
}
.calendar .clndr .clndr-table tr .day.today.event,
.calendar .clndr .clndr-table tr .day.my-today.event {
background: #a7dbc1;
} }
.calendar .clndr .clndr-table tr .day.event, .calendar .clndr .clndr-table tr .day.event,
.calendar .clndr .clndr-table tr .day.my-event { .calendar .clndr .clndr-table tr .day.my-event {
background: #B4E09F; background: #a0b3ff;
} }
.calendar .clndr .clndr-table tr .day.event:hover, .calendar .clndr .clndr-table tr .day.event:hover,
.calendar .clndr .clndr-table tr .day.my-event:hover { .calendar .clndr .clndr-table tr .day.my-event:hover {
background: #96d478; background: #91a3eb;
} }
.calendar .clndr .clndr-table tr .day.inactive, .calendar .clndr .clndr-table tr .day.inactive,
.calendar .clndr .clndr-table tr .day.my-inactive { .calendar .clndr .clndr-table tr .day.my-inactive {
@ -228,3 +219,28 @@ html[xmlns] .clearfix {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;
} }
/*
.calendar .clndr .clndr-table tr .day.selected {
outline: 2px dotted black;
outline-offset: -4px;
}
*/
.calendar {
height: 578px;
overflow: hidden;
}
.clndr > .clndr-main > .events {
display: none;
position: relative;
top: -541px;
height: 541px;
background: #fff;
}
.clndr > .clndr-main.show-event-menu > .events {
display: block;
}

View File

@ -5,3 +5,19 @@ html, body {
background-color: #eee; background-color: #eee;
font-family: "Poppins", sans-serif; font-family: "Poppins", sans-serif;
} }
.container.module {
background-color: white;
border-radius: 6px;
padding: 8px;
}
.content {
display: grid;
grid-template-columns: repeat(4, 25%);
grid-template-rows: repeat(4, 25%);
grid-column-gap: 8px;
grid-row-gap: 8px;
margin-right: 20px;
}

BIN
static/img/login-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

View File

@ -3,7 +3,7 @@
<head> <head>
{% include 'layout/includes/boot-head.html' %} {% include 'layout/includes/boot-head.html' %}
{% if title %} {% if title %}
<title>Husstanden - {{ title }}</title> <title>Husstanden - {{ _(title) }}</title>
{% else %} {% else %}
<title>Husstanden</title> <title>Husstanden</title>
{% endif %} {% endif %}

View File

@ -4,23 +4,45 @@
{% include 'layout/includes/boot-head.html' %} {% include 'layout/includes/boot-head.html' %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/custom.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/custom.css') }}">
{% if title %} {% if title %}
<title>Husstanden - {{ title }}</title> <title>Husstanden - {{ _(title) | title }}</title>
{% else %} {% else %}
<title>Husstanden</title> <title>Husstanden</title>
{% endif %} {% endif %}
<style>
#sidenav-container {
flex: 0 0 320px;
display: none;
overflow: hidden;
}
@media screen and (min-width: 768px){
#sidenav-container {
display: block;
}
}
@media screen and (min-width: 1072px){
#sidenav-container {
}
}
</style>
</head> </head>
<body style="height:100%"> <body style="height:100%">
<div class="p-2" style="height:100%"> <div class="p-2" style="height:100%">
<div class="d-flex" style="height:100%"> <div class="d-flex" style="height:100%">
<div class="col p-2" style="flex:0 0 320px;"> <div class="col p-2" id="sidenav-container">
{% include 'layout/includes/side_nav.html' %} {% include 'layout/includes/side_nav.html' %}
</div> </div>
<div class="col p-2"> <div class="col p-2">
{% include 'layout/includes/top_nav.html' %} {% include 'layout/includes/top_nav.html' %}
<div class="content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</div> </div>
</div> </div>
</div>
{% include 'layout/includes/boot-body.html' %} {% include 'layout/includes/boot-body.html' %}
<script src="{{ url_for('static', filename='js/interact.js') }}"></script> <script src="{{ url_for('static', filename='js/interact.js') }}"></script>
</body> </body>

View File

@ -7,7 +7,7 @@
} }
#sidenav { #sidenav {
background-color: #B507DB; background-color: #506EE4;
border-radius: 4px; border-radius: 4px;
z-index: 100; z-index: 100;
@ -30,7 +30,7 @@
} }
#sidenav a { #sidenav a {
color: #e9d9fc; color: #C5CBE2;
line-height: 1; line-height: 1;
} }
#sidenav a:hover { #sidenav a:hover {
@ -104,55 +104,41 @@
<div class="p-2"> <!-- Padding --> <div class="p-2"> <!-- Padding -->
<div class="flex-column"> <div class="flex-column">
<div> <!-- Collection --> <div> <!-- Collection -->
<a class="item" href="{{ url_for('dashboard') }}">
<i class="far fa-calendar-alt"></i><span>{{ _("Dashboard") }}</span>
</a>
</div>
<div>
<h4>{{ _("Economical") }}</h4>
</div>
<div>
<a class="item" href="{{ url_for('bills') }}">
<i class="fas fa-money-check-alt"></i><span>{{ _("Bills") }}</span>
</a>
</div>
<div>
<a class="item" onclick="toggleCategory(this)"> <a class="item" onclick="toggleCategory(this)">
<i class="fa fa-chart-bar"></i><span>Dashboard</span> <i class="far fa-list-alt"></i><span>{{ _("Receipts") }}</span>
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</a> </a>
<div class="downtab hidden"> <div class="downtab hidden">
<div class="flex-column"> <div class="flex-column">
<div class="page"> <div class="page">
<a href="#dash-test">Test</a> <a href="{{ url_for('receipts') }}">{{ _("Receipts") }}</a>
</div> </div>
<div class="page"> <div class="page">
<a href="#dash-demo">Demo</a> <a href="{{ url_for('warranties') }}">{{ _("Warranties") }}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<h4>Title card</h4> <h4>{{ _("Contacts") }}</h4>
</div> </div>
<div> <div>
<a class="item" onclick="toggleCategory(this)"> <a class="item" href="{{ url_for('services') }}">
<i class="fa fa-chart-bar"></i><span>Bills</span> <i class="fas fa-tools"></i><span>{{ _("Services") }}</span>
<i class="fas fa-chevron-right"></i>
</a> </a>
<div class="downtab hidden">
<div class="flex-column">
<div class="page">
<a href="#bills-bills">Bills</a>
</div>
<div class="page">
<a href="#bills-log">Log</a>
</div>
</div>
</div>
</div>
<div>
<a class="item" onclick="toggleCategory(this)">
<i class="fa fa-chart-bar"></i><span>Recieps</span>
<i class="fas fa-chevron-right"></i>
</a>
<div class="downtab hidden">
<div class="flex-column">
<div class="page">
<a href="#recieps-recieps">Recieps</a>
</div>
<div class="page">
<a href="#recieps-log">Log</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<style> <style>
.rndblock { .rndblock {
border-radius: 100%; border-radius: 100%;
background: #B507DB; background: #506EE4;
background-position: center; background-position: center;
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -11,13 +11,28 @@
font-size: 24px; font-size: 24px;
vertical-align: middle; vertical-align: middle;
display: table-cell; display: table-cell;
cursor: pointer;
} }
.languk { .lang-icon {
background-position: left;
background-size: contain;
width: 32px;
height: 24px;
color: white;
text-align: center;
font-size: 24px;
vertical-align: middle;
display: table-cell;
}
.lang-en {
background-image: url("{{ url_for('static', filename='const/img/flags/gb.svg') }}"); background-image: url("{{ url_for('static', filename='const/img/flags/gb.svg') }}");
} }
.langno { .lang-no {
background-image: url("{{ url_for('static', filename='const/img/flags/no.svg') }}"); background-image: url("{{ url_for('static', filename='const/img/flags/no.svg') }}");
} }
@ -32,10 +47,42 @@
</style> </style>
<nav class="navbar topnav" style="margin-bottom: 10px;"> <nav class="navbar topnav" style="margin-bottom: 10px;">
<h3>{{ title }}</h3> <h3>{{ _(title) | title }}</h3>
<div>
{% with messages = get_flashed_messages(with_categories = true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} fade in show" role="alert" style="margin-bottom:0px;padding:6px;">
<a href="#" class="close" data-dismiss="alert" aria-label="close" style="margin-left:10px;">&times;</a>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div class="my-2 my-lg-0 d-flex icon-buttons"> <div class="my-2 my-lg-0 d-flex icon-buttons">
<div class="col"> <div class="col">
<div class="rndblock languk"></div> <div class="rndblock lang-{{ session.lang }}" data-toggle="dropdown" aria-expanded="false"></div>
<ul class="dropdown-menu">
<li>
<div class="row">
<a class="col d-flex m-1" href="?lang=en">
<div class="lang-icon lang-en"></div>
<div class="flex-grow-1"></div>
<span>{{ _("english") | title }}</span>
<div class="flex-grow-1"></div>
</a>
</div>
<div class="row">
<a class="col d-flex m-1" href="?lang=no">
<div class="lang-icon lang-no"></div>
<div class="flex-grow-1"></div>
<span>{{ _("norwegian") | title }}</span>
<div class="flex-grow-1"></div>
</a>
</div>
</li>
</ul>
</div> </div>
<div class="col"> <div class="col">
<div class="rndblock"> <div class="rndblock">
@ -43,9 +90,18 @@
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="rndblock"> <!-- OR INCLUDE BACKGROUND TEXTURE --> <div class="rndblock" data-toggle="dropdown" aria-expanded="false"> <!-- OR INCLUDE BACKGROUND TEXTURE -->
T <!-- FIRST CHAR OF USER --> {{ current_user.firstname[0]|upper }} <!-- FIRST CHAR OF USER -->
</div> </div>
<ul class="dropdown-menu">
<li>
<div class="row">
<div class="col">
<a href="{{ url_for('logout') }}">{{ _("Sign out") }}</a>
</div>
</div>
</li>
</ul>
</div> </div>
</div> </div>
</nav> </nav>

View File

@ -1,40 +1,254 @@
{% set title = "Login" %} {% set title = _("Login") %}
{% extends "layout/bootstrap.html" %} {% extends "layout/bootstrap.html" %}
{% block content %} {% block content %}
<div class="container pt-3"> <style>
<div class="row alert-section"> html, body {
<div class="col-md-8 mx-auto"> height: 100%;
<div class="card rounded-1"> scroll-behavior: smooth;
<div class="card-header"> }
<h3 class="mb-0">Login Methods</h3>
.main-head{
height: 150px;
background: #FFF;
}
.sidenav {
height: 100%;
background-color: #000;
overflow-x: hidden;
padding-top: 20px;
display: grid;
grid-template-rows: 25% 50% 25%;
height: 100%;
overflow: hidden;
}
.sidenav > div:nth-child(1n) {
grid-row-start: 1;
}
.sidenav > div:nth-child(2n) {
grid-row-start: 2;
align-self: center;
}
.sidenav > div:nth-child(3n) {
grid-row-start: 3;
align-self: end;
}
#main {
padding: 0px 10px;
display: grid;
grid-template-rows: 25% auto;
height: 100%;
}
#main > .col:nth-child(1n) {
grid-row-start: 1;
}
#main > .col:nth-child(2n) {
grid-row-start: 2;
}
#focus-login {
text-decoration: none;
color: #fff;
}
@media screen and (max-height: 450px) {
.sidenav {padding-top: 15px;}
}
@media screen and (max-width: 450px) {
.login-form{
margin-top: 10%;
}
.register-form{
margin-top: 10%;
}
}
@media screen and (min-width: 768px){
#main{
margin-left: 40%;
}
.sidenav {
width: 40%;
position: fixed;
z-index: 1;
top: 0;
left: 0;
}
#focus-login {
display: none;
}
}
.login-main-text{
padding: 60px;
color: #fff;
}
.login-main-text h2{
font-weight: 300;
}
.btn-black{
background-color: #000 !important;
color: #fff;
}
.hidable[aria-expanded="true"] {
display: none;
}
.toggle-form > form:nth-child(2n) {
display: none;
}
.toggle-form.toggled > form:nth-child(1n) {
display: none;
}
.toggle-form.toggled > form:nth-child(2n) {
display: block;
}
.lang-icon {
background-position: left;
background-size: contain;
width: 32px;
height: 24px;
color: white;
text-align: center;
font-size: 24px;
vertical-align: middle;
display: table-cell;
}
.lang-en {
background-image: url("{{ url_for('static', filename='const/img/flags/gb.svg') }}");
}
.lang-no {
background-image: url("{{ url_for('static', filename='const/img/flags/no.svg') }}");
}
.dropdown-menu {
background-color: #6c757d;
}
.dropdown-menu a {
color: white;
}
</style>
<script>
function toggleform(caller) {
let selectedDOM = caller;
do {
if (selectedDOM.classList.contains("toggle-form")){
let classes = selectedDOM.classList;
classes[classes.contains("toggled") ? "remove" : "add"]("toggled");
return;
}
selectedDOM = selectedDOM.parentElement;
} while (selectedDOM != null);
throw Error("Missing toggle-form class for self/parent(s)");
}
</script>
<div class="sidenav">
<div class="login-main-text">
<h2>Husstanden<br>{{ _("Login Page") }}</h2>
<p>{{ _("Login is required to use this service.") }}</p>
</div> </div>
<div class="card-body row"> <div class="text-center mx-auto">
<div class="col-md-6"> <a href="#main" class="display-1" id="focus-login">Login</a>
<h5>Electronic ID</h5>
<p class="lead">
Secure login using Electronic ID allows Husstanden to show you banking details, bills and receipts.<br>
<a target="_blank" href="https://eid.difi.no/en/id-porten/how-obtain-electronic-id">How to obtain an Electronic ID</a>
</p>
</div> </div>
<div class="col-md-6"> <div class="btn-group dropup m-2">
<h5>House Account</h5> <div>
<p class="lead"> <div class="btn btn-secondary col d-flex m-1" data-toggle="dropdown" aria-expanded="false">
Accounts with less privileges and fast login to view non-sensitive information.<br> <div class="lang-icon lang-{{ session.lang }}"></div>
Multiple house accounts can be created by logging in with house holder's verified <i>Electronic ID</i>. <div class="flex-grow-1"></div>
</p> <span>{{ _("Language") }}</span>
<div class="flex-grow-1"></div>
</div>
<ul class="dropdown-menu">
<li>
<div class="row">
<a class="col d-flex m-1" href="?lang=en">
<div class="lang-icon lang-en"></div>
<div class="flex-grow-1"></div>
<span>{{ _("english") | title }}</span>
<div class="flex-grow-1"></div>
</a>
</div>
<div class="row">
<a class="col d-flex m-1" href="?lang=no">
<div class="lang-icon lang-no"></div>
<div class="flex-grow-1"></div>
<span>{{ _("norwegian") | title }}</span>
<div class="flex-grow-1"></div>
</a>
</div>
</li>
</ul>
</div> </div>
</div> </div>
<hr>
<div class="card-body row pt-2">
<div class="col-md-6 pb-2">
<a class="btn btn-primary btn-lg btn-block" href="#TODO_LOGIN_EID" onclick="alertAbove(this, 'warning', 'This should redirect the user to Electronic ID page (but due to phishing, we can not demo this)')">Electronic ID</a>
</div> </div>
<div class="col-md-6"> <div id="main">
<a class="btn btn-primary btn-lg btn-block" id="test" href="#TODO_LOGIN_HA" onclick="alertAbove(this, 'info', 'Unimplemented - Awaiting database')">House Account</a> <div class="col pt-4">
{% with messages = get_flashed_messages(with_categories = true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} fade in show" role="alert" style="margin-bottom:0px;padding:6px;">
<a href="#" class="close" data-dismiss="alert" aria-label="close" style="margin-left:10px;">&times;</a>
{{ message }}
</div> </div>
{% endfor %}
{% endif %}
{% endwith %}
</div> </div>
<div class="col col-md-6 col-sm-12">
<div class="login-form">
{% macro render_field(field) %}
<div class="form-group">
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
<div class="toggle-form false">
<form method="post">
{{ render_field(form.login.email) }}
{{ render_field(form.login.password) }}
<button class="btn btn-black">{{ _("Login") }}</button>
<span class="btn btn-secondary" onclick="toggleform(this)">{{ _("Register") }}</span>
</form>
<form method="post">
{{ render_field(form.register.email) }}
{{ render_field(form.register.password) }}
{{ render_field(form.register.confirm_password) }}
{{ render_field(form.register.firstname) }}
{{ render_field(form.register.surname) }}
{{ render_field(form.register.accept_tos) }}
<span class="btn btn-secondary" onclick="toggleform(this)">{{ _("Login") }}</span>
<button class="btn btn-black">{{ _("Register") }}</button>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,21 +1,89 @@
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/calendar.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/calendar.css') }}">
<div class="calendar"></div> <div class="calendar">
<script type="text/template" id="tmplt-clndr">
<div class='clndr-controls'>
<div class='clndr-control-button'>
<span class='clndr-previous-button'>&lsaquo; previous</span>
</div>
<div class='month'><%= month %> <%= year %></div>
<div class='clndr-control-button rightalign'>
<span class='clndr-next-button'>next &rsaquo;</span>
</div>
</div>
<div class="clndr-main">
<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>
<thead>
<tr class='header-days'>
<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>
<td class='header-day'><%= daysOfTheWeek[i] %></td>
<% } %>
</tr>
</thead>
<tbody>
<% for(var i = 0; i < numberOfRows; i++){ %>
<tr>
<% for(var j = 0; j < 7; j++){ %>
<% var d = j + i * 7; %>
<td class='<%= days[d].classes %>'>
<div class='day-contents'><%= days[d].day %></div>
</td>
<% } %>
</tr>
<% } %>
</tbody>
</table>
</div>
</script>
<script type="text/template" id="tmplt-clndr-events">
<div class='clndr-controls'>
<div class='clndr-control-button'>
<span class='clndr-previous-button'>&lsaquo; previous</span>
</div>
<div class='month'><%= extras.selected.format("LL") %></div>
<div class='clndr-control-button rightalign'>
<span class='clndr-next-button'>next &rsaquo;</span>
</div>
</div>
<div class="clndr-main">
<table class='clndr-table' border='0' cellspacing='0' cellpadding='0'>
<thead>
<tr class='header-days'>
<% for(var i = 0; i < daysOfTheWeek.length; i++) { %>
<td class='header-day'><%= daysOfTheWeek[i] %></td>
<% } %>
</tr>
</thead>
<tbody>
<% for(var i = 0; i < numberOfRows; i++){ %>
<tr>
<% for(var j = 0; j < 7; j++){ %>
<% var d = j + i * 7; %>
<td class='<%= days[d].classes %>'>
<div class='day-contents'><%= days[d].day %></div>
</td>
<% } %>
</tr>
<% } %>
</tbody>
</table>
</div>
</script>
</div>
<script src="{{ url_for('static', filename='js/moment.min.js') }}"></script> <script src="{{ url_for('static', filename='js/moment.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/underscore.min.js') }}"></script> <script src="{{ url_for('static', filename='js/underscore.min.js') }}"></script>
<script src="{{ url_for('static', filename='modules/clndr.min.js') }}"></script> <script src="{{ url_for('static', filename='modules/clndr.min.js') }}"></script>
<script> <script>
var calendars = {}; var calendars = {};
var tmplts = {};
var a;
$(document).ready( function() { $(document).ready( function() {
// Assuming you've got the appropriate language files, tmplts.clndr = $('#tmplt-clndr').html();
// clndr will respect whatever moment's language is set to. tmplts.clndrEvents = $('#tmplt-clndr-events').html();
// moment.locale('ru');
// Here's some magic to make sure the dates are happening this month.
var thisMonth = moment().format('YYYY-MM'); var thisMonth = moment().format('YYYY-MM');
// Events to load into calendar
var eventArray = [ // TODO: Get events from database var eventArray = [ // TODO: Get events from database
{ {
title: 'Multi-Day Event', title: 'Multi-Day Event',
@ -28,19 +96,28 @@ $(document).ready( function() {
}, { }, {
date: thisMonth + '-27', date: thisMonth + '-27',
title: 'Single Day Event' title: 'Single Day Event'
}, {
date: thisMonth + '-6',
title: 'Test'
} }
]; ];
// The order of the click handlers is predictable. Direct click action
// callbacks come first: click, nextMonth, previousMonth, nextYear,
// previousYear, nextInterval, previousInterval, or today. Then
// onMonthChange (if the month changed), inIntervalChange if the interval
// has changed, and finally onYearChange (if the year changed).
calendars.clndr = $('.calendar').clndr({ calendars.clndr = $('.calendar').clndr({
events: eventArray, events: eventArray,
clickEvents: { clickEvents: {
click: function (target) { click: function (target) {
console.log('Cal-1 clicked: ', target); a = target;
console.log(target.date.day());
this.options.extras.selected = target.date;
this.compiledClndrTemplate = _.template(tmplts.clndrEvents);
this.render();
/*
var clndrContainer = $('.calendar').find('.clndr-main');
clndrContainer.toggleClass('show-event-menu', true);
$('.calendar').find('.x-button').click( function() {
clndrContainer.toggleClass('show-event-menu', false);
});
*/
}, },
today: function () { today: function () {
console.log('Cal-1 today'); console.log('Cal-1 today');
@ -53,24 +130,6 @@ $(document).ready( function() {
}, },
onMonthChange: function () { onMonthChange: function () {
console.log('Cal-1 month changed'); console.log('Cal-1 month changed');
},
nextYear: function () {
console.log('Cal-1 next year');
},
previousYear: function () {
console.log('Cal-1 previous year');
},
onYearChange: function () {
console.log('Cal-1 year changed');
},
nextInterval: function () {
console.log('Cal-1 next interval');
},
previousInterval: function () {
console.log('Cal-1 previous interval');
},
onIntervalChange: function () {
console.log('Cal-1 interval changed');
} }
}, },
multiDayEvents: { multiDayEvents: {
@ -79,17 +138,19 @@ $(document).ready( function() {
startDate: 'startDate' startDate: 'startDate'
}, },
showAdjacentMonths: true, showAdjacentMonths: true,
adjacentDaysChangeMonth: false adjacentDaysChangeMonth: false,
weekOffset: 1,
forceSixRows: true,
template: tmplts.clndr,
extras: {
selected: null
}
}); });
// Bind all clndrs to the left and right arrow keys
$(document).keydown( function(e) { $(document).keydown( function(e) {
// Left arrow
if (e.keyCode == 37) { if (e.keyCode == 37) {
calendars.clndr.back(); calendars.clndr.back();
} }
// Right arrow
if (e.keyCode == 39) { if (e.keyCode == 39) {
calendars.clndr.forward(); calendars.clndr.forward();
} }

View File

@ -1,11 +0,0 @@
{% set title = "bill:)" %}
{% extends "layout/bootstrap.html" %}
{% block content %}
<p>lotsa bills</p>
<p>many bills</p>
<i class="fas fa-user"></i> <!-- uses solid style -->
<i class="far fa-user"></i> <!-- uses regular style -->
<i class="fab fa-github-square"></i> <!-- uses brands style -->
{% endblock %}

View File

@ -0,0 +1,84 @@
{% set title = _("Bills") %}
{% extends "layout/dash.html" %}
{% block content %}
<style id="grid">
.container.module:nth-child(1n) {
grid-area: 1 / 1 / 5 / 5;
}
</style>
<div class="container module">
<button type="button" class="btn btn-primary" style="margin:10px;" data-toggle="modal" data-target="#myModal">{{ _("Add") }}</button>
<div class="modal fade" id="myModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ _("Add bill") }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
{% macro render_field(field) %}
<div class="form-group">
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
<form method=post>
<dl>
{{ render_field(form.payment_to) }}
{{ render_field(form.description) }}
{{ render_field(form.sum) }}
{{ render_field(form.kid) }}
{{ render_field(form.date_due) }}
</dl>
<input type=submit value="{{ _('Add') }}">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
</div>
</div>
</div>
</div>
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col">{{ _("Payment to") }}</th>
<th scope="col">{{ _("Description") }}</th>
<th scope="col" style="width: 120px">{{ _("Sum") }}</th>
<th scope="col" style="width: 220px">{{ _("KID") }}</th>
<th scope="col" style="width: 120px">{{ _("Date due") }}</th>
<th scope="col" style="width: 150px">{{ _("Payment status") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<!--<tr class="table-{{ 'success' if row[5] else 'danger' }}">-->
<tr>
<th>{{ row[0] }}</th>
<td>{{ row[1] }}</td>
<td>{{ row[2] }}</td>
<td>{{ row[3] }}</td>
<td>{{ row[4] }}</td>
<td style="background-color:{{ '#4F4' if row[5] else '#F44' }};"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,9 +1,31 @@
{% set title = "Dashboard" %} {% set title = _("Dashboard") %}
{% extends "layout/dash.html" %} {% extends "layout/dash.html" %}
{% block content %} {% block content %}
<div class="container module">
<h4>Reminders</h4>
</div>
<div class="container module">
<h4>Calendar</h4>
{% include "modules/calendar.html" %} {% include "modules/calendar.html" %}
</div>
<style id="grid">
.container.module:nth-child(1n) {
grid-area: 1 / 1 / 5 / 2;
}
.container.module:nth-child(2n) {
grid-area: 1 / 2 / 5 / 5;
}
</style>
<script>
function onDaySelect() {
}
</script>
{% endblock %} {% endblock %}

View File

@ -1,10 +0,0 @@
{% set title = "Kvitteringer" %}
{% extends "layout/bootstrap.html" %}
{% block content %}
<p>Tester siden!</p>
<p>KVITTERINGER FOR FAEN!</p>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% set title = _("Receipts") %}
{% extends "layout/dash.html" %}
{% block content %}
<style id="grid">
.container.module:nth-child(1n) {
grid-area: 1 / 1 / 5 / 5;
}
</style>
<div class="container module">
<a>Unfinished</a>
</div>
{% endblock %}

View File

@ -1,8 +0,0 @@
{% set title = "Regninger" %}
{% extends "layout/bootstrap.html" %}
{% block content %}
<p>Regninger :D</p>
<p>test</p>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% set title = _("Services") %}
{% extends "layout/dash.html" %}
{% block content %}
<style id="grid">
.container.module:nth-child(1n) {
grid-area: 1 / 1 / 5 / 5;
}
</style>
<div class="container module">
<button type="button" class="btn btn-primary" style="margin:10px;" data-toggle="modal" data-target="#myModal">{{ _("Add") }}</button>
<div class="modal fade" id="myModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ _("Add service") }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
{% macro render_field(field) %}
<div class="form-group">
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
<form method=post>
<dl>
{{ render_field(form.name) }}
{{ render_field(form.type) }}
{{ render_field(form.contact) }}
{{ render_field(form.phone) }}
{{ render_field(form.website) }}
</dl>
<input type=submit value="{{ _('Add') }}">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
</div>
</div>
</div>
</div>
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col">{{ _("Name") }}</th>
<th scope="col">{{ _("Type") }}</th>
<th scope="col">{{ _("Contact") }}</th>
<th scope="col">{{ _("Phone") }}</th>
<th scope="col">{{ _("Website") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<!--<tr class="table-{{ 'success' if row[5] else 'danger' }}">-->
<tr>
<th>{{ row[0] }}</th>
<td>{{ row[1] }}</td>
<td>{{ row[2] }}</td>
<td>{{ row[3] }}</td>
<td>{{ row[4] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,8 +0,0 @@
{% set title = "testing" %}
{% extends "layout/bootstrap.html" %}
{% block content %}
<p>testing</p>
<p>tester dette</p>
{% endblock %}

View File

@ -0,0 +1,79 @@
{% set title = _("Warranties") %}
{% extends "layout/dash.html" %}
{% block content %}
<style id="grid">
.container.module:nth-child(1n) {
grid-area: 1 / 1 / 5 / 5;
}
</style>
<div class="container module">
<button type="button" class="btn btn-primary" style="margin:10px;" data-toggle="modal" data-target="#myModal">{{ _("Add") }}</button>
<div class="modal fade" id="myModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ _("Add warrenty") }}</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
{% macro render_field(field) %}
<div class="form-group">
<label for="{{ field.label.field_id }}">{{ _(field.label.text) }}</label>
{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
<form method=post>
<dl>
{{ render_field(form.item) }}
{{ render_field(form.date_from) }}
{{ render_field(form.date_to) }}
{{ render_field(form.sum) }}
</dl>
<input type=submit value="{{ _('Add') }}">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
</div>
</div>
</div>
</div>
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col">{{ _("Item") }}</th>
<th scope="col" style="width: 140px">{{ _("Date of purchase") }}</th>
<th scope="col" style="width: 140px">{{ _("Warranty duration") }}</th>
<th scope="col" style="width: 140px">{{ _("Sum") }}</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<!--<tr class="table-{{ 'success' if row[5] else 'danger' }}">-->
<tr>
<th>{{ row[0] }}</th>
<td>{{ row[1] }}</td>
<td>{{ row[2] }}</td>
<td>{{ row[3] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

55
templates/register.html Normal file
View File

@ -0,0 +1,55 @@
{% set title = "Register" %}
{% extends "layout/bootstrap.html" %}
{% block content %}
{% macro render_field(field) %}
<dt>{{ field.label }}
<dd>{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</dd>
{% endmacro %}
<script src="{{ url_for('static', filename='js/alerts.js') }}"></script>
<div class="container pt-3">
<div class="row alert-section">
<div class="col-md-8 mx-auto">
<div class="card rounded-1">
<div class="card-header">
<h3 class="mb-0">Register</h3>
</div>
<div class="card-body row">
<div class="col-md-6">
<form method=post>
<dl>
{{ render_field(form.email) }}
{{ render_field(form.password) }}
{{ render_field(form.confirm_password) }}
{{ render_field(form.firstname) }}
{{ render_field(form.surname) }}
{{ render_field(form.accept_tos) }}
</dl>
<input type=submit value="Register">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
{% with messages = get_flashed_messages() %}
{% if messages %}
let alertArea = document.getElementsByClassName("alert-section")[0];
{% for message in messages %}
alertAbove(alertArea, "info", "{{ message }}");
{% endfor %}
{% endif %}
{% endwith %}
</script>
{% endblock %}

View File

@ -0,0 +1,215 @@
# Norwegian text and messages
#
msgid ""
msgstr ""
"Project-Id-Version: 0.4\n"
"Report-Msgid-Bugs-To: noreply@osufx.com\n"
"POT-Creation-Date: 2019-05-28 19:59+0200\n"
"PO-Revision-Date: 2019-05-12 09:47+1000\n"
"Last-Translator: Emily Steinsvik <emily@osufx.com>\n"
"Language: no\n"
"Language-Team: no <emily@osufx.com>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.7.0\n"
#: routes.py:131
msgid "Already logged in"
msgstr "Allerede logget inn"
#: routes.py:141
msgid "User registered"
msgstr "Bruker registrert"
#: routes.py:150
msgid "Logged in"
msgstr "Logget inn"
#: routes.py:164
msgid "Logged out"
msgstr "Logget ut"
#: routes.py:169
msgid "Login is required"
msgstr "Innlogging kreves"
#: forms/login.py:14 templates/pages/bills.html:59
msgid "Payment to"
msgstr "Betaling til"
#: forms/login.py:15 templates/pages/bills.html:60
msgid "Description"
msgstr "Tekst"
#: forms/login.py:20 forms/login.py:28 templates/pages/bills.html:61
#: templates/pages/warranties.html:61
msgid "Sum"
msgstr "Sum"
#: forms/login.py:21 templates/pages/bills.html:62
msgid "KID"
msgstr "KID"
#: forms/login.py:22 templates/pages/bills.html:63
msgid "Date due"
msgstr "Forfallsdato"
#: forms/login.py:25 templates/pages/warranties.html:58
msgid "Item"
msgstr "Vare"
#: forms/login.py:26 templates/pages/warranties.html:59
msgid "Date of purchase"
msgstr "Kjøpsdato"
#: forms/login.py:27 templates/pages/warranties.html:60
msgid "Warranty duration"
msgstr "Garanti til"
#: forms/login.py:31 templates/pages/services.html:59
msgid "Name"
msgstr "Navn"
#: forms/login.py:32 templates/pages/services.html:60
msgid "Type"
msgstr "Type"
#: forms/login.py:33 templates/pages/services.html:61
msgid "Contact"
msgstr "Kontaktperson"
#: forms/login.py:34 templates/pages/services.html:62
msgid "Phone"
msgstr "Telefon"
#: forms/login.py:35 templates/pages/services.html:63
msgid "Website"
msgstr "Nettside"
#: forms/login.py:38 forms/login.py:51
msgid "Email"
msgstr "Epost"
#: forms/login.py:44 forms/login.py:57
msgid "Password"
msgstr "Passord"
#: forms/login.py:60
msgid "Passwords must match"
msgstr "Passordene må være like"
#: forms/login.py:63
msgid "Repeat Password"
msgstr "Gjenta passord"
#: forms/login.py:65
msgid "Firstname"
msgstr "Fornavn"
#: forms/login.py:71
msgid "Surname"
msgstr "Etternavn"
#: forms/login.py:77
msgid "I accept the TOS"
msgstr "Jeg aksepterer TOS"
#: forms/login.py:104
msgid "Invalid login"
msgstr "Ugyldig innlogging"
#: forms/login.py:107
msgid "Incorrect password"
msgstr "Feil passord"
#: templates/login.html:1 templates/login.html:239 templates/login.html:249
msgid "Login"
msgstr "Login"
#: templates/login.html:170
msgid "Login Page"
msgstr "Innlogging"
#: templates/login.html:171
msgid "Login is required to use this service."
msgstr "Innlogging kreves for å bruke denne tjenesten."
#: templates/login.html:181
msgid "Language"
msgstr "Språk"
#: templates/layout/includes/top_nav.html:72 templates/login.html:190
msgid "english"
msgstr "engelsk"
#: templates/layout/includes/top_nav.html:80 templates/login.html:198
msgid "norwegian"
msgstr "norsk"
#: templates/login.html:240 templates/login.html:250
msgid "Register"
msgstr "Registrer"
#: templates/layout/includes/side_nav.html:108 templates/pages/dashboard.html:1
msgid "Dashboard"
msgstr "Dashbord"
#: templates/layout/includes/side_nav.html:112
msgid "Economical"
msgstr "Økonomisk"
#: templates/layout/includes/side_nav.html:116 templates/pages/bills.html:1
msgid "Bills"
msgstr "Regninger"
#: templates/layout/includes/side_nav.html:121
#: templates/layout/includes/side_nav.html:127 templates/pages/receipts.html:1
msgid "Receipts"
msgstr "Kvitteringer"
#: templates/layout/includes/side_nav.html:130
#: templates/pages/warranties.html:1
msgid "Warranties"
msgstr "Garantier"
#: templates/layout/includes/side_nav.html:136
msgid "Contacts"
msgstr "Kontakter"
#: templates/layout/includes/side_nav.html:140 templates/pages/services.html:1
msgid "Services"
msgstr "Service"
#: templates/layout/includes/top_nav.html:100
msgid "Sign out"
msgstr "Logg ut"
#: templates/pages/bills.html:15 templates/pages/bills.html:46
#: templates/pages/services.html:15 templates/pages/services.html:46
#: templates/pages/warranties.html:15 templates/pages/warranties.html:45
msgid "Add"
msgstr "Legg til"
#: templates/pages/bills.html:21
msgid "Add bill"
msgstr "Legg til regning"
#: templates/pages/bills.html:50 templates/pages/services.html:50
#: templates/pages/warranties.html:49
msgid "Close"
msgstr "Lukk"
#: templates/pages/bills.html:64
msgid "Payment status"
msgstr "Betalingsstatus"
#: templates/pages/services.html:21
msgid "Add service"
msgstr "Legg til service"
#: templates/pages/warranties.html:21
msgid "Add warrenty"
msgstr "Legg til garanti"