Merge branch 'feature/server-side-render-devices' of github.com:eReuse/devicehub-teal into feature/server-side-render-devices
|
@ -66,6 +66,7 @@ jobs:
|
|||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export SECRET_KEY=`python3 -c 'import secrets; print(secrets.token_hex())'`
|
||||
source env/bin/activate
|
||||
coverage run --source='ereuse_devicehub' env/bin/pytest -m mvp --maxfail=5 tests/
|
||||
coverage report --include='ereuse_devicehub/*'
|
||||
|
|
|
@ -7,10 +7,10 @@ import click
|
|||
import click_spinner
|
||||
import ereuse_utils.cli
|
||||
from ereuse_utils.session import DevicehubClient
|
||||
from flask.globals import _app_ctx_stack, g
|
||||
from flask_login import current_user
|
||||
from flask import g as gg
|
||||
from flask import _app_ctx_stack, g
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from teal.db import SchemaSQLAlchemy
|
||||
from teal.teal import Teal
|
||||
|
||||
|
@ -21,12 +21,8 @@ from ereuse_devicehub.db import db
|
|||
from ereuse_devicehub.dummy.dummy import Dummy
|
||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
|
||||
from ereuse_devicehub.templating import Environment
|
||||
|
||||
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
from ereuse_devicehub.templating import Environment
|
||||
|
||||
|
||||
class Devicehub(Teal):
|
||||
|
@ -80,7 +76,6 @@ class Devicehub(Teal):
|
|||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
gg.user = current_user
|
||||
return User.query.get(user_id)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
|
@ -172,6 +167,9 @@ class Devicehub(Teal):
|
|||
inv = g.inventory = Inventory.current # type: Inventory
|
||||
g.tag_provider = DevicehubClient(base_url=inv.tag_provider,
|
||||
token=DevicehubClient.encode_token(inv.tag_token))
|
||||
# NOTE: models init methods expects that current user is
|
||||
# available on g.user (e.g. to initialize object owner)
|
||||
g.user = current_user
|
||||
|
||||
def create_client(self, email='user@dhub.com', password='1234'):
|
||||
client = UserClient(self, email, password, response_wrapper=self.response_class)
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from werkzeug.security import generate_password_hash
|
||||
from wtforms import EmailField, PasswordField, validators
|
||||
from wtforms import BooleanField, EmailField, PasswordField, validators
|
||||
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
email = EmailField('Email Address', [validators.Length(min=6, max=35)])
|
||||
password = PasswordField('Password', [
|
||||
validators.DataRequired(),
|
||||
])
|
||||
password = PasswordField('Password', [validators.DataRequired()])
|
||||
remember = BooleanField('Remember me')
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': (
|
||||
|
|
|
@ -71,6 +71,12 @@ class User(UserMixin, Thing):
|
|||
"""Alias because flask-login expects `is_active` attribute"""
|
||||
return self.active
|
||||
|
||||
@property
|
||||
def get_full_name(self):
|
||||
# TODO(@slamora) create first_name & last_name fields and use
|
||||
# them to generate user full name
|
||||
return self.email
|
||||
|
||||
def check_password(self, password):
|
||||
# take advantage of SQL Alchemy PasswordType to verify password
|
||||
return self.password == password
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 738 B |
Before Width: | Height: | Size: 4.8 KiB |
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 59.641495 19.325608"
|
||||
height="19.325607mm"
|
||||
width="59.641495mm">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(-149.20084,-143.42372)"
|
||||
id="layer1">
|
||||
<text
|
||||
id="text823"
|
||||
y="158.73648"
|
||||
x="80.130806"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-size:22.57777786px;stroke-width:0.26458332"
|
||||
id="tspan821"
|
||||
y="158.73648"
|
||||
x="147.32671"><tspan
|
||||
id="tspan819"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.57777786px;font-family:Calibri;-inkscape-font-specification:Calibri;fill:#993366;fill-opacity:1;stroke-width:0.26458332"
|
||||
y="158.73648"
|
||||
x="147.32671"><tspan
|
||||
id="tspan825"
|
||||
style="fill:#0a0406;fill-opacity:1">USO</tspan>dy</tspan></tspan></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -5,7 +5,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
|
||||
<title>Users / Profile - NiceAdmin Bootstrap Template</title>
|
||||
<title>{% block page_title %}{% endblock %} - USOdy</title>
|
||||
<meta content="" name="description">
|
||||
<meta content="" name="keywords">
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<a href="index.html" class="logo d-flex align-items-center">
|
||||
<img src="{{ url_for('static', filename='img/logo.png') }}" alt="">
|
||||
<span class="d-none d-lg-block">NiceAdmin</span>
|
||||
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
|
||||
</a>
|
||||
<i class="bi bi-list toggle-sidebar-btn"></i>
|
||||
</div><!-- End Logo -->
|
||||
|
@ -27,165 +26,23 @@
|
|||
</a>
|
||||
</li><!-- End Search Icon-->
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
<a class="nav-link nav-icon" href="#" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-bell"></i>
|
||||
<span class="badge bg-primary badge-number">4</span>
|
||||
</a><!-- End Notification Icon -->
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow notifications">
|
||||
<li class="dropdown-header">
|
||||
You have 4 new notifications
|
||||
<a href="#"><span class="badge rounded-pill bg-primary p-2 ms-2">View all</span></a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="notification-item">
|
||||
<i class="bi bi-exclamation-circle text-warning"></i>
|
||||
<div>
|
||||
<h4>Lorem Ipsum</h4>
|
||||
<p>Quae dolorem earum veritatis oditseno</p>
|
||||
<p>30 min. ago</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="notification-item">
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
<div>
|
||||
<h4>Atque rerum nesciunt</h4>
|
||||
<p>Quae dolorem earum veritatis oditseno</p>
|
||||
<p>1 hr. ago</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="notification-item">
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
<div>
|
||||
<h4>Sit rerum fuga</h4>
|
||||
<p>Quae dolorem earum veritatis oditseno</p>
|
||||
<p>2 hrs. ago</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="notification-item">
|
||||
<i class="bi bi-info-circle text-primary"></i>
|
||||
<div>
|
||||
<h4>Dicta reprehenderit</h4>
|
||||
<p>Quae dolorem earum veritatis oditseno</p>
|
||||
<p>4 hrs. ago</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li class="dropdown-footer">
|
||||
<a href="#">Show all notifications</a>
|
||||
</li>
|
||||
|
||||
</ul><!-- End Notification Dropdown Items -->
|
||||
|
||||
</li><!-- End Notification Nav -->
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
<a class="nav-link nav-icon" href="#" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-chat-left-text"></i>
|
||||
<span class="badge bg-success badge-number">3</span>
|
||||
</a><!-- End Messages Icon -->
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow messages">
|
||||
<li class="dropdown-header">
|
||||
You have 3 new messages
|
||||
<a href="#"><span class="badge rounded-pill bg-primary p-2 ms-2">View all</span></a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="message-item">
|
||||
<a href="#">
|
||||
<img src="{{ url_for('static', filename='img/messages-1.jpg') }}" alt="" class="rounded-circle">
|
||||
<div>
|
||||
<h4>Maria Hudson</h4>
|
||||
<p>Velit asperiores et ducimus soluta repudiandae labore officia est ut...</p>
|
||||
<p>4 hrs. ago</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="message-item">
|
||||
<a href="#">
|
||||
<img src="{{ url_for('static', filename='img/messages-2.jpg') }}" alt="" class="rounded-circle">
|
||||
<div>
|
||||
<h4>Anna Nelson</h4>
|
||||
<p>Velit asperiores et ducimus soluta repudiandae labore officia est ut...</p>
|
||||
<p>6 hrs. ago</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="message-item">
|
||||
<a href="#">
|
||||
<img src="{{ url_for('static', filename='img/messages-3.jpg') }}" alt="" class="rounded-circle">
|
||||
<div>
|
||||
<h4>David Muldon</h4>
|
||||
<p>Velit asperiores et ducimus soluta repudiandae labore officia est ut...</p>
|
||||
<p>8 hrs. ago</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li class="dropdown-footer">
|
||||
<a href="#">Show all messages</a>
|
||||
</li>
|
||||
|
||||
</ul><!-- End Messages Dropdown Items -->
|
||||
|
||||
</li><!-- End Messages Nav -->
|
||||
|
||||
<li class="nav-item dropdown pe-3">
|
||||
|
||||
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">
|
||||
<img src="{{ url_for('static', filename='img/profile-img.jpg') }}" alt="Profile" class="rounded-circle">
|
||||
<span class="d-none d-md-block dropdown-toggle ps-2">K. Anderson</span>
|
||||
<i class="bi bi-person-circle" style="font-size: 36px;"></i>
|
||||
<span class="d-none d-md-block dropdown-toggle ps-2">{{ current_user.email }}</span>
|
||||
</a><!-- End Profile Iamge Icon -->
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
|
||||
<li class="dropdown-header">
|
||||
<h6>Kevin Anderson</h6>
|
||||
<span>Web Designer</span>
|
||||
<h6>{{ current_user.get_full_name }}</h6>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="users-profile.html">
|
||||
<a class="dropdown-item d-flex align-items-center" href="{{ url_for('core.user-profile') }}">
|
||||
<i class="bi bi-person"></i>
|
||||
<span>My Profile</span>
|
||||
</a>
|
||||
|
@ -194,16 +51,6 @@
|
|||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="users-profile.html">
|
||||
<i class="bi bi-gear"></i>
|
||||
<span>Account Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="pages-faq.html">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
|
@ -215,7 +62,7 @@
|
|||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<a class="dropdown-item d-flex align-items-center" href="{{ url_for('core.logout') }}">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span>Sign Out</span>
|
||||
</a>
|
||||
|
@ -316,7 +163,7 @@
|
|||
</li><!-- End Labels Page Nav -->
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#TODO">
|
||||
<a class="nav-link collapsed" href="{{ url_for('Document.StampsView') }}">
|
||||
<i class="bi bi-pin-map-fill"></i>
|
||||
<span>Stamp</span>
|
||||
</a>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{% extends "ereuse_devicehub/base.html" %}
|
||||
|
||||
{% block page_title %}Login{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main>
|
||||
<div class="container">
|
||||
|
@ -9,9 +12,8 @@
|
|||
<div class="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">
|
||||
|
||||
<div class="d-flex justify-content-center py-4">
|
||||
<a href="index.html" class="logo d-flex align-items-center w-auto">
|
||||
<img src="{{ url_for('static', filename='img/logo.png') }}" alt="">
|
||||
<span class="d-none d-lg-block">NiceAdmin</span>
|
||||
<a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
|
||||
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
|
||||
</a>
|
||||
</div><!-- End Logo -->
|
||||
|
||||
|
@ -35,12 +37,9 @@
|
|||
{{ form.csrf_token }}
|
||||
|
||||
<div class="col-12">
|
||||
<label for="yourUsername" class="form-label">Email</label>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text" id="inputGroupPrepend">@</span>
|
||||
<input type="text" name="email" class="form-control" id="yourUsername" required value="{{ form.email.data|default('', true) }}">
|
||||
<div class="invalid-feedback">Please enter your username.</div>
|
||||
</div>
|
||||
<label for="yourEmail" class="form-label">Email</label>
|
||||
<input type="email" name="email" class="form-control" id="yourEmail" required value="{{ form.email.data|default('', true) }}">
|
||||
<div class="invalid-feedback">Please enter your email.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
|
@ -51,7 +50,7 @@
|
|||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="remember" value="true" id="rememberMe">
|
||||
<input class="form-check-input" type="checkbox" name="remember" {% if form.remember.data %}checked{% endif %} id="rememberMe">
|
||||
<label class="form-check-label" for="rememberMe">Remember me</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
{% extends "ereuse_devicehub/base_site.html" %}
|
||||
|
||||
{% block page_title %}Your Profile{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<div class="pagetitle">
|
||||
|
@ -18,16 +21,8 @@
|
|||
|
||||
<div class="card">
|
||||
<div class="card-body profile-card pt-4 d-flex flex-column align-items-center">
|
||||
|
||||
<img src="{{ url_for('static', filename='img/profile-img.jpg') }}" alt="Profile" class="rounded-circle">
|
||||
<h2>Kevin Anderson</h2>
|
||||
<h3>Web Designer</h3>
|
||||
<div class="social-links mt-2">
|
||||
<a href="#" class="twitter"><i class="bi bi-twitter"></i></a>
|
||||
<a href="#" class="facebook"><i class="bi bi-facebook"></i></a>
|
||||
<a href="#" class="instagram"><i class="bi bi-instagram"></i></a>
|
||||
<a href="#" class="linkedin"><i class="bi bi-linkedin"></i></a>
|
||||
</div>
|
||||
<i class="bi bi-person-circle" style="font-size: 76px;"></i>
|
||||
<h2>{{ current_user.get_full_name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import flask
|
||||
from flask import Blueprint
|
||||
from flask.views import View
|
||||
from flask_login import login_required, login_user
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
|
||||
from ereuse_devicehub.forms import LoginForm
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
@ -20,7 +20,7 @@ class LoginView(View):
|
|||
# Login and validate the user.
|
||||
# user should be an instance of your `User` class
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
login_user(user)
|
||||
login_user(user, remember=form.remember.data)
|
||||
|
||||
next_url = flask.request.args.get('next')
|
||||
# is_safe_url should check if the url is safe for redirects.
|
||||
|
@ -32,14 +32,23 @@ class LoginView(View):
|
|||
return flask.render_template('ereuse_devicehub/user_login.html', form=form)
|
||||
|
||||
|
||||
class LogoutView(View):
|
||||
def dispatch_request(self):
|
||||
logout_user()
|
||||
return flask.redirect(flask.url_for('core.login'))
|
||||
|
||||
|
||||
class UserProfileView(View):
|
||||
decorators = [login_required]
|
||||
template_name = 'ereuse_devicehub/user_profile.html'
|
||||
|
||||
def dispatch_request(self):
|
||||
context = {}
|
||||
context = {
|
||||
'current_user': current_user,
|
||||
}
|
||||
return flask.render_template(self.template_name, **context)
|
||||
|
||||
|
||||
core.add_url_rule('/login/', view_func=LoginView.as_view('login'))
|
||||
core.add_url_rule('/logout/', view_func=LogoutView.as_view('logout'))
|
||||
core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile'))
|
||||
|
|