diff --git a/BABEL_USAGE.md b/BABEL_USAGE.md new file mode 100644 index 0000000..3b7e9d1 --- /dev/null +++ b/BABEL_USAGE.md @@ -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` diff --git a/README.md b/README.md index 753028a..ffb6939 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,13 @@ Install the required dependencies with pip ``` $ 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 ``` -Configuration can be edited in config.json \ No newline at end of file +Configuration can be edited in config.json diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..0cc0bac --- /dev/null +++ b/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ \ No newline at end of file diff --git a/default_config.json b/default_config.json index aa69d5d..614eac6 100644 --- a/default_config.json +++ b/default_config.json @@ -11,5 +11,12 @@ "auto_pull_and_restart": false, "webhook_endpoint": "/api/git_commit", "secret": "iOnlyHavePullAccess" + }, + "mysql": { + "host": "localhost", + "port": 3306, + "user": "husstanden", + "passwd": "", + "db": "db_husstanden" } } \ No newline at end of file diff --git a/filters.py b/filters.py deleted file mode 100644 index fb8cceb..0000000 --- a/filters.py +++ /dev/null @@ -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) -""" \ No newline at end of file diff --git a/forms/login.py b/forms/login.py new file mode 100644 index 0000000..14ba8a0 --- /dev/null +++ b/forms/login.py @@ -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() diff --git a/init.sql b/init.sql index e5c9be5..cb887f6 100644 --- a/init.sql +++ b/init.sql @@ -5101,7 +5101,7 @@ CREATE TABLE Husrelasjoner ( BrukerID INT(11), HusID INT(11), - Privleges INT(11), + Privileges INT(11), PRIMARY KEY (BrukerID, HusID), CONSTRAINT FOREIGN KEY (BrukerID) REFERENCES Bruker(BrukerID), CONSTRAINT FOREIGN KEY (HusID) REFERENCES Husstand(HusID) diff --git a/localizer.py b/localizer.py new file mode 100644 index 0000000..d4a71ea --- /dev/null +++ b/localizer.py @@ -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") diff --git a/main.py b/main.py index 8c09a6f..d1f7bb2 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,11 @@ from flask import Flask, url_for, request from objects import glob # Global sharing of python objects in a manageable way glob.app = Flask(__name__) +glob.app.secret_key = "E2FGrJXLtOxPh70Q" +import localizer # Initialize localization (Babel) 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) @glob.app.route(glob.config["git"]["webhook_endpoint"], methods = ["POST"]) diff --git a/objects/glob.py b/objects/glob.py index 08a05e6..1400ea4 100644 --- a/objects/glob.py +++ b/objects/glob.py @@ -1,11 +1,14 @@ import os import json import shutil +import mysql.connector +import bcrypt # ------------------------------------------------------------------------------ # Global variables that is None by default and gets overwritten in other modules app = None # main.py -> Flask App +sql_conn = None # ------------------------------------------------------------------------------ # Global variables that initializes on first load of module @@ -15,4 +18,19 @@ if not os.path.isfile("config.json"): shutil.copy("default_config.json", "config.json") with open("config.json", "r") as f: - config = json.load(f) \ No newline at end of file + 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()) diff --git a/requirements.txt b/requirements.txt index 44e7a67..db893c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ Flask>=1.0.2 Flask-WTF>=0.14.2 +flask_login>=0.4.1 +Flask-Babel>=0.12.2 +mysql-connector +bcrypt \ No newline at end of file diff --git a/routes.py b/routes.py index 269a169..3ca514e 100644 --- a/routes.py +++ b/routes.py @@ -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 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("/home") @glob.app.route("/dashboard") -def home(): +@flask_login.login_required +def dashboard(): return render_template("pages/dashboard.html") -@glob.app.route("/login", methods = ["GET", "POST"]) -def serve_login(): - if request.method == "POST": - return "TODO: Login handle", 501 - return render_template("login.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"]) +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 form_register.validate(): + 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")) diff --git a/static/css/calendar.css b/static/css/calendar.css index 7a587f8..a41634a 100644 --- a/static/css/calendar.css +++ b/static/css/calendar.css @@ -120,7 +120,7 @@ html[xmlns] .clearfix { .calendar .clndr .clndr-table .header-days { height: 30px; font-size: 10px; - background: #0D70A6; + background: #506EE4; } .calendar .clndr .clndr-table .header-days .header-day { vertical-align: middle; @@ -139,8 +139,7 @@ html[xmlns] .clearfix { vertical-align: top; } .calendar .clndr .clndr-table tr .day { - border-left: 1px solid #000000; - border-top: 1px solid #000000; + border: 1px solid #000000; width: 100%; height: inherit; } @@ -149,23 +148,15 @@ html[xmlns] .clearfix { } .calendar .clndr .clndr-table tr .day.today, .calendar .clndr .clndr-table tr .day.my-today { - background: #9AD6E3; -} -.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; + box-shadow: inset 0 0 0 2px black; } .calendar .clndr .clndr-table tr .day.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.my-event:hover { - background: #96d478; + background: #91a3eb; } .calendar .clndr .clndr-table tr .day.inactive, .calendar .clndr .clndr-table tr .day.my-inactive { @@ -228,3 +219,28 @@ html[xmlns] .clearfix { opacity: 0.5; 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; +} \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 923b54b..71b1071 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -4,4 +4,20 @@ html, body { height: 100%; background-color: #eee; font-family: "Poppins", sans-serif; -} \ No newline at end of file +} + +.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; +} + diff --git a/static/img/login-bg.jpg b/static/img/login-bg.jpg new file mode 100644 index 0000000..bccd6a5 Binary files /dev/null and b/static/img/login-bg.jpg differ diff --git a/templates/layout/bootstrap.html b/templates/layout/bootstrap.html index 48653a8..a4027b8 100644 --- a/templates/layout/bootstrap.html +++ b/templates/layout/bootstrap.html @@ -3,7 +3,7 @@ {% include 'layout/includes/boot-head.html' %} {% if title %} - Husstanden - {{ title }} + Husstanden - {{ _(title) }} {% else %} Husstanden {% endif %} diff --git a/templates/layout/dash.html b/templates/layout/dash.html index cc0ed3a..c05f385 100644 --- a/templates/layout/dash.html +++ b/templates/layout/dash.html @@ -4,20 +4,42 @@ {% include 'layout/includes/boot-head.html' %} {% if title %} - Husstanden - {{ title }} + Husstanden - {{ _(title) | title }} {% else %} Husstanden {% endif %} +
-
+
{% include 'layout/includes/side_nav.html' %}
{% include 'layout/includes/top_nav.html' %} +
{% block content %}{% endblock %} +
diff --git a/templates/layout/includes/side_nav.html b/templates/layout/includes/side_nav.html index bb8fdd7..827f40a 100644 --- a/templates/layout/includes/side_nav.html +++ b/templates/layout/includes/side_nav.html @@ -7,7 +7,7 @@ } #sidenav { - background-color: #B507DB; + background-color: #506EE4; border-radius: 4px; z-index: 100; @@ -30,7 +30,7 @@ } #sidenav a { - color: #e9d9fc; + color: #C5CBE2; line-height: 1; } #sidenav a:hover { @@ -104,55 +104,41 @@
diff --git a/templates/layout/includes/top_nav.html b/templates/layout/includes/top_nav.html index 811b549..e1018b4 100644 --- a/templates/layout/includes/top_nav.html +++ b/templates/layout/includes/top_nav.html @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index d2a17bc..6084208 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,40 +1,254 @@ -{% set title = "Login" %} +{% set title = _("Login") %} {% extends "layout/bootstrap.html" %} {% block content %} -
-
-
-
-
-

Login Methods

-
-
-
-
Electronic ID
-

- Secure login using Electronic ID allows Husstanden to show you banking details, bills and receipts.
- How to obtain an Electronic ID -

+ + +
+ +
+ Login +
+
+
+ + +
+
+
+
+
+{% with messages = get_flashed_messages(with_categories = true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} +{% endwith %} +
+
+
diff --git a/templates/modules/calendar.html b/templates/modules/calendar.html index 385c7b9..110a244 100644 --- a/templates/modules/calendar.html +++ b/templates/modules/calendar.html @@ -1,21 +1,89 @@ -
+
+ + +
{% endblock %} \ No newline at end of file diff --git a/templates/pages/kvittering.html b/templates/pages/kvittering.html deleted file mode 100644 index 5743bef..0000000 --- a/templates/pages/kvittering.html +++ /dev/null @@ -1,10 +0,0 @@ -{% set title = "Kvitteringer" %} - -{% extends "layout/bootstrap.html" %} - -{% block content %} - -

Tester siden!

-

KVITTERINGER FOR FAEN!

- -{% endblock %} \ No newline at end of file diff --git a/templates/pages/receipts.html b/templates/pages/receipts.html new file mode 100644 index 0000000..cb3be56 --- /dev/null +++ b/templates/pages/receipts.html @@ -0,0 +1,17 @@ +{% set title = _("Receipts") %} + +{% extends "layout/dash.html" %} + +{% block content %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/regninger.html b/templates/pages/regninger.html deleted file mode 100644 index 87e0e34..0000000 --- a/templates/pages/regninger.html +++ /dev/null @@ -1,8 +0,0 @@ -{% set title = "Regninger" %} - -{% extends "layout/bootstrap.html" %} - -{% block content %} -

Regninger :D

-

test

-{% endblock %} \ No newline at end of file diff --git a/templates/pages/services.html b/templates/pages/services.html new file mode 100644 index 0000000..44b8ea9 --- /dev/null +++ b/templates/pages/services.html @@ -0,0 +1,82 @@ +{% set title = _("Services") %} + +{% extends "layout/dash.html" %} + +{% block content %} + + + +
+ + + + + + + + + + + + + + + +{% for row in data %} + + + + + + + + +{% endfor %} + +
{{ _("Name") }}{{ _("Type") }}{{ _("Contact") }}{{ _("Phone") }}{{ _("Website") }}
{{ row[0] }}{{ row[1] }}{{ row[2] }}{{ row[3] }}{{ row[4] }}
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/pages/testing.html b/templates/pages/testing.html deleted file mode 100644 index 91a7ad0..0000000 --- a/templates/pages/testing.html +++ /dev/null @@ -1,8 +0,0 @@ -{% set title = "testing" %} - -{% extends "layout/bootstrap.html" %} - -{% block content %} -

testing

-

tester dette

-{% endblock %} \ No newline at end of file diff --git a/templates/pages/warranties.html b/templates/pages/warranties.html new file mode 100644 index 0000000..9c040e5 --- /dev/null +++ b/templates/pages/warranties.html @@ -0,0 +1,79 @@ +{% set title = _("Warranties") %} + +{% extends "layout/dash.html" %} + +{% block content %} + + + +
+ + + + + + + + + + + + + + +{% for row in data %} + + + + + + + +{% endfor %} + +
{{ _("Item") }}{{ _("Date of purchase") }}{{ _("Warranty duration") }}{{ _("Sum") }}
{{ row[0] }}{{ row[1] }}{{ row[2] }}{{ row[3] }}
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..a0462e7 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,55 @@ +{% set title = "Register" %} + +{% extends "layout/bootstrap.html" %} + +{% block content %} +{% macro render_field(field) %} +
{{ field.label }} +
{{ field(**kwargs)|safe }} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+{% endmacro %} + +
+
+
+
+
+

Register

+
+
+
+
+
+ {{ 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) }} +
+ +
+
+
+
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/translations/nb/LC_MESSAGES/messages.po b/translations/nb/LC_MESSAGES/messages.po new file mode 100644 index 0000000..cc21173 --- /dev/null +++ b/translations/nb/LC_MESSAGES/messages.po @@ -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 \n" +"Language: no\n" +"Language-Team: no \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" +