Merge branch 'feature/server-side-render-devices' of github.com:eReuse/devicehub-teal into feature/server-side-render-devices

This commit is contained in:
Cayo Puigdefabregas 2022-01-04 12:45:36 +01:00
commit 8333d6804c
14 changed files with 98 additions and 197 deletions

View File

@ -66,6 +66,7 @@ jobs:
- name: Run Tests - name: Run Tests
run: | run: |
export SECRET_KEY=`python3 -c 'import secrets; print(secrets.token_hex())'`
source env/bin/activate source env/bin/activate
coverage run --source='ereuse_devicehub' env/bin/pytest -m mvp --maxfail=5 tests/ coverage run --source='ereuse_devicehub' env/bin/pytest -m mvp --maxfail=5 tests/
coverage report --include='ereuse_devicehub/*' coverage report --include='ereuse_devicehub/*'

View File

@ -7,10 +7,10 @@ import click
import click_spinner import click_spinner
import ereuse_utils.cli import ereuse_utils.cli
from ereuse_utils.session import DevicehubClient from ereuse_utils.session import DevicehubClient
from flask.globals import _app_ctx_stack, g from flask import _app_ctx_stack, g
from flask_login import current_user from flask_login import LoginManager, current_user
from flask import g as gg
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from teal.db import SchemaSQLAlchemy from teal.db import SchemaSQLAlchemy
from teal.teal import Teal 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.dummy.dummy import Dummy
from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef 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.resources.user.models import User
from ereuse_devicehub.templating import Environment
class Devicehub(Teal): class Devicehub(Teal):
@ -80,7 +76,6 @@ class Devicehub(Teal):
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
gg.user = current_user
return User.query.get(user_id) return User.query.get(user_id)
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
@ -172,6 +167,9 @@ class Devicehub(Teal):
inv = g.inventory = Inventory.current # type: Inventory inv = g.inventory = Inventory.current # type: Inventory
g.tag_provider = DevicehubClient(base_url=inv.tag_provider, g.tag_provider = DevicehubClient(base_url=inv.tag_provider,
token=DevicehubClient.encode_token(inv.tag_token)) 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'): def create_client(self, email='user@dhub.com', password='1234'):
client = UserClient(self, email, password, response_wrapper=self.response_class) client = UserClient(self, email, password, response_wrapper=self.response_class)

View File

@ -1,15 +1,14 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from werkzeug.security import generate_password_hash 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 from ereuse_devicehub.resources.user.models import User
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
email = EmailField('Email Address', [validators.Length(min=6, max=35)]) email = EmailField('Email Address', [validators.Length(min=6, max=35)])
password = PasswordField('Password', [ password = PasswordField('Password', [validators.DataRequired()])
validators.DataRequired(), remember = BooleanField('Remember me')
])
error_messages = { error_messages = {
'invalid_login': ( 'invalid_login': (

View File

@ -71,6 +71,12 @@ class User(UserMixin, Thing):
"""Alias because flask-login expects `is_active` attribute""" """Alias because flask-login expects `is_active` attribute"""
return self.active 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): def check_password(self, password):
# take advantage of SQL Alchemy PasswordType to verify password # take advantage of SQL Alchemy PasswordType to verify password
return self.password == password return self.password == password

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -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

View File

@ -5,7 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport"> <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="description">
<meta content="" name="keywords"> <meta content="" name="keywords">

View File

@ -5,8 +5,7 @@
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<a href="index.html" class="logo d-flex align-items-center"> <a href="index.html" class="logo d-flex align-items-center">
<img src="{{ url_for('static', filename='img/logo.png') }}" alt=""> <img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
<span class="d-none d-lg-block">NiceAdmin</span>
</a> </a>
<i class="bi bi-list toggle-sidebar-btn"></i> <i class="bi bi-list toggle-sidebar-btn"></i>
</div><!-- End Logo --> </div><!-- End Logo -->
@ -27,165 +26,23 @@
</a> </a>
</li><!-- End Search Icon--> </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"> <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"> <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"> <i class="bi bi-person-circle" style="font-size: 36px;"></i>
<span class="d-none d-md-block dropdown-toggle ps-2">K. Anderson</span> <span class="d-none d-md-block dropdown-toggle ps-2">{{ current_user.email }}</span>
</a><!-- End Profile Iamge Icon --> </a><!-- End Profile Iamge Icon -->
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile"> <ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
<li class="dropdown-header"> <li class="dropdown-header">
<h6>Kevin Anderson</h6> <h6>{{ current_user.get_full_name }}</h6>
<span>Web Designer</span>
</li> </li>
<li> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </li>
<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> <i class="bi bi-person"></i>
<span>My Profile</span> <span>My Profile</span>
</a> </a>
@ -194,16 +51,6 @@
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </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> <li>
<a class="dropdown-item d-flex align-items-center" href="pages-faq.html"> <a class="dropdown-item d-flex align-items-center" href="pages-faq.html">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
@ -215,7 +62,7 @@
</li> </li>
<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> <i class="bi bi-box-arrow-right"></i>
<span>Sign Out</span> <span>Sign Out</span>
</a> </a>
@ -316,7 +163,7 @@
</li><!-- End Labels Page Nav --> </li><!-- End Labels Page Nav -->
<li class="nav-item"> <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> <i class="bi bi-pin-map-fill"></i>
<span>Stamp</span> <span>Stamp</span>
</a> </a>

View File

@ -1,4 +1,7 @@
{% extends "ereuse_devicehub/base.html" %} {% extends "ereuse_devicehub/base.html" %}
{% block page_title %}Login{% endblock %}
{% block body %} {% block body %}
<main> <main>
<div class="container"> <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="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"> <div class="d-flex justify-content-center py-4">
<a href="index.html" class="logo d-flex align-items-center w-auto"> <a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
<img src="{{ url_for('static', filename='img/logo.png') }}" alt=""> <img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
<span class="d-none d-lg-block">NiceAdmin</span>
</a> </a>
</div><!-- End Logo --> </div><!-- End Logo -->
@ -35,12 +37,9 @@
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="col-12"> <div class="col-12">
<label for="yourUsername" class="form-label">Email</label> <label for="yourEmail" class="form-label">Email</label>
<div class="input-group has-validation"> <input type="email" name="email" class="form-control" id="yourEmail" required value="{{ form.email.data|default('', true) }}">
<span class="input-group-text" id="inputGroupPrepend">@</span> <div class="invalid-feedback">Please enter your email.</div>
<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>
</div> </div>
<div class="col-12"> <div class="col-12">
@ -51,7 +50,7 @@
<div class="col-12"> <div class="col-12">
<div class="form-check"> <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> <label class="form-check-label" for="rememberMe">Remember me</label>
</div> </div>
</div> </div>

View File

@ -1,4 +1,7 @@
{% extends "ereuse_devicehub/base_site.html" %} {% extends "ereuse_devicehub/base_site.html" %}
{% block page_title %}Your Profile{% endblock %}
{% block main %} {% block main %}
<div class="pagetitle"> <div class="pagetitle">
@ -18,16 +21,8 @@
<div class="card"> <div class="card">
<div class="card-body profile-card pt-4 d-flex flex-column align-items-center"> <div class="card-body profile-card pt-4 d-flex flex-column align-items-center">
<i class="bi bi-person-circle" style="font-size: 76px;"></i>
<img src="{{ url_for('static', filename='img/profile-img.jpg') }}" alt="Profile" class="rounded-circle"> <h2>{{ current_user.get_full_name }}</h2>
<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>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import flask import flask
from flask import Blueprint from flask import Blueprint
from flask.views import View 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.forms import LoginForm
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
@ -20,7 +20,7 @@ class LoginView(View):
# Login and validate the user. # Login and validate the user.
# user should be an instance of your `User` class # user should be an instance of your `User` class
user = User.query.filter_by(email=form.email.data).first() 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') next_url = flask.request.args.get('next')
# is_safe_url should check if the url is safe for redirects. # 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) 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): class UserProfileView(View):
decorators = [login_required] decorators = [login_required]
template_name = 'ereuse_devicehub/user_profile.html' template_name = 'ereuse_devicehub/user_profile.html'
def dispatch_request(self): def dispatch_request(self):
context = {} context = {
'current_user': current_user,
}
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
core.add_url_rule('/login/', view_func=LoginView.as_view('login')) 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')) core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile'))