Skip to content
Snippets Groups Projects
Commit 3050ce17 authored by Benjamin Bertrand's avatar Benjamin Bertrand
Browse files

Add attributes favorites page

- create many-to-many relationships between the user table and
attributes tables (manufacturer / model / location / status / action)
- add a checkbox as the first column of the attributes table to add or
remove an attribute to the favorites
- add a new page to display the favorite attributes

JIRA INFRA-283
parent 1a22bbce
No related branches found
No related tags found
No related merge requests found
......@@ -12,7 +12,7 @@ This module implements the inventory blueprint.
import sqlalchemy as sa
from flask import (Blueprint, render_template, jsonify, session,
request, redirect, url_for, flash, current_app)
from flask_login import login_required
from flask_login import login_required, current_user
from .forms import AttributeForm, ItemForm, CommentForm
from ..extensions import db
from ..decorators import login_groups_accepted
......@@ -205,6 +205,26 @@ def edit_item(ics_id):
return render_template('inventory/edit_item.html', form=form)
@bp.route('/attributes/favorites')
@login_required
def attributes_favorites():
return render_template('inventory/attributes_favorites.html')
@bp.route('/_retrieve_attributes_favorites')
@login_required
def retrieve_attributes_favorites():
if current_user not in db.session:
# If the current user is cached, it won't be in the sqlalchemy session
# Add it to access the user favorite attributes relationship
db.session.add(current_user)
data = [(favorite.base64_image(),
type(favorite).__name__,
favorite.name,
favorite.description) for favorite in current_user.favorite_attributes()]
return jsonify(data=data)
@bp.route('/attributes/<kind>', methods=('GET', 'POST'))
@login_groups_accepted('admin', 'create')
def attributes(kind):
......@@ -233,10 +253,44 @@ def retrieve_attributes(kind):
except AttributeError:
raise utils.CSEntryError(f"Unknown model '{kind}'", status_code=422)
items = db.session.query(model).order_by(model.name)
data = [(item.base64_image(), item.name, item.description) for item in items]
data = [({'id': item.id, 'favorite': item.is_user_favorite()},
item.base64_image(),
item.name,
item.description) for item in items]
return jsonify(data=data)
@bp.route('/_update_favorites/<kind>', methods=['POST'])
@login_required
def update_favorites(kind):
"""Update the current user favorite attributes
Add or remove the attribute from the favorites when the
checkbox is checked/unchecked in the attributes table
"""
if current_user not in db.session:
# If the current user is cached, it won't be in the sqlalchemy session
# Add it to access the user favorite attributes relationship
db.session.add(current_user)
try:
model = getattr(models, kind)
except AttributeError:
raise utils.CSEntryError(f"Unknown model '{kind}'", status_code=422)
data = request.get_json()
attribute = model.query.get(data['id'])
favorite_attributes_str = utils.pluralize(f'favorite_{kind.lower()}')
user_favorite_attributes = getattr(current_user, favorite_attributes_str)
if data['checked']:
user_favorite_attributes.append(attribute)
message = 'Attribute added to the favorites'
else:
user_favorite_attributes.remove(attribute)
message = 'Attribute removed from the favorites'
db.session.commit()
data = {'message': message}
return jsonify(data=data), 201
@bp.route('/scanner')
@login_required
def scanner():
......
......@@ -95,6 +95,34 @@ def save_user(dn, username, data, memberships):
return user
# Tables required for Many-to-Many relationships between users and favorites attributes
favorite_manufacturers_table = db.Table(
'favorite_manufacturers',
db.Column('user_id', db.Integer, db.ForeignKey('user_account.id'), primary_key=True),
db.Column('manufacturer_id', db.Integer, db.ForeignKey('manufacturer.id'), primary_key=True)
)
favorite_models_table = db.Table(
'favorite_models',
db.Column('user_id', db.Integer, db.ForeignKey('user_account.id'), primary_key=True),
db.Column('model_id', db.Integer, db.ForeignKey('model.id'), primary_key=True)
)
favorite_locations_table = db.Table(
'favorite_locations',
db.Column('user_id', db.Integer, db.ForeignKey('user_account.id'), primary_key=True),
db.Column('location_id', db.Integer, db.ForeignKey('location.id'), primary_key=True)
)
favorite_statuses_table = db.Table(
'favorite_statuses',
db.Column('user_id', db.Integer, db.ForeignKey('user_account.id'), primary_key=True),
db.Column('status_id', db.Integer, db.ForeignKey('status.id'), primary_key=True)
)
favorite_actions_table = db.Table(
'favorite_actions',
db.Column('user_id', db.Integer, db.ForeignKey('user_account.id'), primary_key=True),
db.Column('action_id', db.Integer, db.ForeignKey('action.id'), primary_key=True)
)
class User(db.Model, UserMixin):
# "user" is a reserved word in postgresql
# so let's use another name
......@@ -106,6 +134,33 @@ class User(db.Model, UserMixin):
email = db.Column(db.Text)
groups = db.Column(postgresql.ARRAY(db.Text), default=[])
tokens = db.relationship("Token", backref="user")
# The favorites won't be accessed very often so we load them
# only when necessary (lazy=True)
favorite_manufacturers = db.relationship(
'Manufacturer',
secondary=favorite_manufacturers_table,
lazy=True,
backref=db.backref('favorite_users', lazy=True))
favorite_models = db.relationship(
'Model',
secondary=favorite_models_table,
lazy=True,
backref=db.backref('favorite_users', lazy=True))
favorite_locations = db.relationship(
'Location',
secondary=favorite_locations_table,
lazy=True,
backref=db.backref('favorite_users', lazy=True))
favorite_statuses = db.relationship(
'Status',
secondary=favorite_statuses_table,
lazy=True,
backref=db.backref('favorite_users', lazy=True))
favorite_actions = db.relationship(
'Action',
secondary=favorite_actions_table,
lazy=True,
backref=db.backref('favorite_users', lazy=True))
def get_id(self):
"""Return the user id as unicode
......@@ -137,6 +192,13 @@ class User(db.Model, UserMixin):
names.extend(current_app.config['CSENTRY_LDAP_GROUPS'].get(group))
return bool(set(self.groups) & set(names))
def favorite_attributes(self):
"""Return all user's favorite attributes"""
favorites_list = [self.favorite_manufacturers, self.favorite_models,
self.favorite_locations, self.favorite_statuses,
self.favorite_actions]
return [favorite for favorites in favorites_list for favorite in favorites]
def __str__(self):
return self.username
......@@ -190,6 +252,11 @@ class QRCodeMixin:
"""Return the QRCode image as base64 string"""
return utils.image_to_base64(self.image())
def is_user_favorite(self):
"""Return True if the attribute is part of the current user favorites"""
user = utils.cse_current_user()
return user in self.favorite_users
def __str__(self):
return self.name
......
......@@ -9,6 +9,54 @@ $(document).ready(function() {
callback(json);
});
},
"order": [[2, 'asc']],
"columnDefs": [
{
"targets": [0],
"orderable": false,
'className': 'text-center align-middle',
"render": function(data, type, row) {
// render a checkbox to add/remove the attribute to the user's favorites
var checked = data.favorite ? "checked" : ""
return '<input type="checkbox" value="' + data.id + '" ' + checked + '>'
},
"width": "5%",
},
{
"targets": [1],
"orderable": false,
"render": function(data, type, row) {
// render QR code from base64 string
return '<img class="img-fluid" src="data:image/png;base64,' + data + '">';
},
"width": "10%",
}
],
"paging": false
});
// update the user favorites
$("#attributes_table").on('change', 'input[type="checkbox"]', function() {
var kind = $('li a.nav-link.active').text();
$.ajax({
type: "POST",
url: $SCRIPT_ROOT + "/inventory/_update_favorites/" + kind ,
data: JSON.stringify({
id: $(this).val(),
checked: this.checked
}),
contentType : 'application/json'
});
});
var attributes_favorites_table = $("#attributes_favorites_table").DataTable({
"ajax": function(data, callback, settings) {
$.getJSON(
$SCRIPT_ROOT + "/inventory/_retrieve_attributes_favorites",
function(json) {
callback(json);
});
},
"order": [[1, 'asc']],
"columnDefs": [
{
......
......@@ -10,10 +10,14 @@
<a class="nav-link {% if attribute == kind %}active{% endif %}" href="{{ url_for('inventory.attributes', kind=attribute) }}">{{ attribute }}</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link {{ is_active(request.path.startswith("/inventory/attributes/favorites")) }}" href="{{ url_for('inventory.attributes_favorites') }}">Favorites</a>
</li>
</ul>
<br>
{% block attributes_main %}
{% if kind in ('Manufacturer', 'Model', 'Location') or (current_user.is_authenticated and current_user.is_admin and kind != 'Action') %}
<form id="attributeForm" method="POST">
{{ form.hidden_tag() }}
......@@ -32,12 +36,14 @@
<table id="attributes_table" class="table table-bordered table-hover table-sm" cellspacing="0" width="100%">
<thead>
<tr>
<th></th>
<th>Favorite</th>
<th>QRCode</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
</table>
{%- endblock %}
{%- endblock %}
{% block csentry_scripts %}
......
{% extends "inventory/attributes.html" %}
{% block title %}Attributes favorites - CSEntry{% endblock %}
{% block attributes_main %}
<table id="attributes_favorites_table" class="table table-bordered table-hover table-sm" cellspacing="0" width="100%">
<thead>
<tr>
<th>QRCode</th>
<th>Kind</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
</table>
{%- endblock %}
......@@ -198,3 +198,14 @@ def random_mac():
random.randint(0x00, 0xFF)]
octets = [f'{nb:02x}' for nb in octets]
return ':'.join((current_app.config['MAC_OUI'], *octets))
def pluralize(singular):
"""Return the plural form of the given word
Used to pluralize API endpoints (not any given english word)
"""
if not singular.endswith('s'):
return singular + 's'
else:
return singular + 'es'
"""Add user favorite attributes tables
Revision ID: a73eeb144fa1
Revises: ac6b3c416b07
Create Date: 2018-04-07 21:23:33.337335
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a73eeb144fa1'
down_revision = 'ac6b3c416b07'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'favorite_actions',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('action_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['action_id'], ['action.id'], name=op.f('fk_favorite_actions_action_id_action')),
sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], name=op.f('fk_favorite_actions_user_id_user_account')),
sa.PrimaryKeyConstraint('user_id', 'action_id', name=op.f('pk_favorite_actions'))
)
op.create_table(
'favorite_locations',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('location_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['location_id'], ['location.id'], name=op.f('fk_favorite_locations_location_id_location')),
sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], name=op.f('fk_favorite_locations_user_id_user_account')),
sa.PrimaryKeyConstraint('user_id', 'location_id', name=op.f('pk_favorite_locations'))
)
op.create_table(
'favorite_manufacturers',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('manufacturer_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['manufacturer_id'], ['manufacturer.id'], name=op.f('fk_favorite_manufacturers_manufacturer_id_manufacturer')),
sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], name=op.f('fk_favorite_manufacturers_user_id_user_account')),
sa.PrimaryKeyConstraint('user_id', 'manufacturer_id', name=op.f('pk_favorite_manufacturers'))
)
op.create_table(
'favorite_models',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('model_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['model_id'], ['model.id'], name=op.f('fk_favorite_models_model_id_model')),
sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], name=op.f('fk_favorite_models_user_id_user_account')),
sa.PrimaryKeyConstraint('user_id', 'model_id', name=op.f('pk_favorite_models'))
)
op.create_table(
'favorite_statuses',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('status_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['status_id'], ['status.id'], name=op.f('fk_favorite_statuses_status_id_status')),
sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], name=op.f('fk_favorite_statuses_user_id_user_account')),
sa.PrimaryKeyConstraint('user_id', 'status_id', name=op.f('pk_favorite_statuses'))
)
def downgrade():
op.drop_table('favorite_statuses')
op.drop_table('favorite_models')
op.drop_table('favorite_manufacturers')
op.drop_table('favorite_locations')
op.drop_table('favorite_actions')
......@@ -104,3 +104,20 @@ def test_mac_address_validation(mac_factory):
with pytest.raises(ValidationError) as excinfo:
mac = mac_factory(address='F4A73915DA')
assert "'F4A73915DA' does not appear to be a MAC address" in str(excinfo.value)
def test_manufacturer_favorite_users(user_factory, manufacturer_factory):
user1 = user_factory()
user2 = user_factory()
user3 = user_factory()
manufacturer1 = manufacturer_factory()
manufacturer2 = manufacturer_factory()
user1.favorite_manufacturers.append(manufacturer2)
assert manufacturer1.favorite_users == []
assert manufacturer2.favorite_users == [user1]
user2.favorite_manufacturers.append(manufacturer2)
user3.favorite_manufacturers.append(manufacturer1)
assert manufacturer2.favorite_users == [user1, user2]
assert user2 in manufacturer2.favorite_users
assert user2 not in manufacturer1.favorite_users
assert user3 in manufacturer1.favorite_users
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment