Merge commit '8d8ce634023e71d463f87527097c3ec8a2cdd7cb' into feature/confirm-trade-changes
This commit is contained in:
commit
1ff0401b38
|
@ -1,7 +1,9 @@
|
||||||
|
from flask import g
|
||||||
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 BooleanField, EmailField, PasswordField, validators
|
from wtforms import BooleanField, EmailField, PasswordField, validators
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,3 +61,43 @@ class LoginForm(FlaskForm):
|
||||||
self.form_errors.append(self.error_messages['inactive'])
|
self.form_errors.append(self.error_messages['inactive'])
|
||||||
|
|
||||||
return user.is_active
|
return user.is_active
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordForm(FlaskForm):
|
||||||
|
password = PasswordField(
|
||||||
|
'Current Password',
|
||||||
|
[validators.DataRequired()],
|
||||||
|
render_kw={'class': "form-control"},
|
||||||
|
)
|
||||||
|
newpassword = PasswordField(
|
||||||
|
'New Password',
|
||||||
|
[validators.DataRequired()],
|
||||||
|
render_kw={'class': "form-control"},
|
||||||
|
)
|
||||||
|
renewpassword = PasswordField(
|
||||||
|
'Re-enter New Password',
|
||||||
|
[validators.DataRequired()],
|
||||||
|
render_kw={'class': "form-control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, extra_validators=None):
|
||||||
|
is_valid = super().validate(extra_validators)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not g.user.check_password(self.password.data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.newpassword.data != self.renewpassword.data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
g.user.password = self.newpassword.data
|
||||||
|
|
||||||
|
db.session.add(g.user)
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
return
|
||||||
|
|
|
@ -3,7 +3,9 @@ from operator import attrgetter
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
from sqlalchemy import Column
|
||||||
|
from sqlalchemy import Enum as DBEnum
|
||||||
|
from sqlalchemy import ForeignKey, Unicode, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.orm import backref, relationship, validates
|
from sqlalchemy.orm import backref, relationship, validates
|
||||||
|
@ -31,7 +33,7 @@ class Agent(Thing):
|
||||||
name = Column(CIText())
|
name = Column(CIText())
|
||||||
name.comment = """The name of the organization or person."""
|
name.comment = """The name of the organization or person."""
|
||||||
tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id'))
|
tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id'))
|
||||||
tax_id.comment = """The Tax / Fiscal ID of the organization,
|
tax_id.comment = """The Tax / Fiscal ID of the organization,
|
||||||
e.g. the TIN in the US or the CIF/NIF in Spain.
|
e.g. the TIN in the US or the CIF/NIF in Spain.
|
||||||
"""
|
"""
|
||||||
country = Column(DBEnum(enums.Country))
|
country = Column(DBEnum(enums.Country))
|
||||||
|
@ -42,7 +44,7 @@ class Agent(Thing):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||||
UniqueConstraint(tax_id, name, name='One tax ID with one name.'),
|
UniqueConstraint(tax_id, name, name='One tax ID with one name.'),
|
||||||
db.Index('agent_type', type, postgresql_using='hash')
|
db.Index('agent_type', type, postgresql_using='hash'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
@ -63,7 +65,9 @@ class Agent(Thing):
|
||||||
@property
|
@property
|
||||||
def actions(self) -> list:
|
def actions(self) -> list:
|
||||||
# todo test
|
# todo test
|
||||||
return sorted(chain(self.actions_agent, self.actions_to), key=attrgetter('created'))
|
return sorted(
|
||||||
|
chain(self.actions_agent, self.actions_to), key=attrgetter('created')
|
||||||
|
)
|
||||||
|
|
||||||
@validates('name')
|
@validates('name')
|
||||||
def does_not_contain_slash(self, _, value: str):
|
def does_not_contain_slash(self, _, value: str):
|
||||||
|
@ -76,15 +80,17 @@ class Agent(Thing):
|
||||||
|
|
||||||
|
|
||||||
class Organization(JoinedTableMixin, Agent):
|
class Organization(JoinedTableMixin, Agent):
|
||||||
default_of = db.relationship(Inventory,
|
default_of = db.relationship(
|
||||||
uselist=False,
|
Inventory,
|
||||||
lazy=True,
|
uselist=False,
|
||||||
backref=backref('org', lazy=True),
|
lazy=True,
|
||||||
# We need to use this as we cannot do Inventory.foreign -> Org
|
backref=backref('org', lazy=True),
|
||||||
# as foreign keys can only reference to one table
|
# We need to use this as we cannot do Inventory.foreign -> Org
|
||||||
# and we have multiple organization table (one per schema)
|
# as foreign keys can only reference to one table
|
||||||
foreign_keys=[Inventory.org_id],
|
# and we have multiple organization table (one per schema)
|
||||||
primaryjoin=lambda: Organization.id == Inventory.org_id)
|
foreign_keys=[Inventory.org_id],
|
||||||
|
primaryjoin=lambda: Organization.id == Inventory.org_id,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, name: str, **kwargs) -> None:
|
def __init__(self, name: str, **kwargs) -> None:
|
||||||
super().__init__(**kwargs, name=name)
|
super().__init__(**kwargs, name=name)
|
||||||
|
@ -97,12 +103,17 @@ class Organization(JoinedTableMixin, Agent):
|
||||||
|
|
||||||
class Individual(JoinedTableMixin, Agent):
|
class Individual(JoinedTableMixin, Agent):
|
||||||
active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id))
|
active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id))
|
||||||
active_org = relationship(Organization, primaryjoin=active_org_id == Organization.id)
|
|
||||||
|
active_org = relationship(
|
||||||
|
Organization, primaryjoin=active_org_id == Organization.id
|
||||||
|
)
|
||||||
|
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True)
|
user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True)
|
||||||
user = relationship(User,
|
user = relationship(
|
||||||
backref=backref('individuals', lazy=True, collection_class=set),
|
User,
|
||||||
primaryjoin=user_id == User.id)
|
backref=backref('individuals', lazy=True, collection_class=set),
|
||||||
|
primaryjoin=user_id == User.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Membership(Thing):
|
class Membership(Thing):
|
||||||
|
@ -110,20 +121,29 @@ class Membership(Thing):
|
||||||
|
|
||||||
For example, because the individual works in or because is a member of.
|
For example, because the individual works in or because is a member of.
|
||||||
"""
|
"""
|
||||||
id = Column(Unicode(), check_lower('id'))
|
|
||||||
organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True)
|
|
||||||
organization = relationship(Organization,
|
|
||||||
backref=backref('members', collection_class=set, lazy=True),
|
|
||||||
primaryjoin=organization_id == Organization.id)
|
|
||||||
individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True)
|
|
||||||
individual = relationship(Individual,
|
|
||||||
backref=backref('member_of', collection_class=set, lazy=True),
|
|
||||||
primaryjoin=individual_id == Individual.id)
|
|
||||||
|
|
||||||
def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None:
|
id = Column(Unicode(), check_lower('id'))
|
||||||
super().__init__(organization=organization,
|
organization_id = Column(
|
||||||
individual=individual,
|
UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True
|
||||||
id=id)
|
)
|
||||||
|
organization = relationship(
|
||||||
|
Organization,
|
||||||
|
backref=backref('members', collection_class=set, lazy=True),
|
||||||
|
primaryjoin=organization_id == Organization.id,
|
||||||
|
)
|
||||||
|
individual_id = Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True
|
||||||
|
)
|
||||||
|
individual = relationship(
|
||||||
|
Individual,
|
||||||
|
backref=backref('member_of', collection_class=set, lazy=True),
|
||||||
|
primaryjoin=individual_id == Individual.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, organization: Organization, individual: Individual, id: str = None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(organization=organization, individual=individual, id=id)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(id, organization_id, name='One member id per organization.'),
|
UniqueConstraint(id, organization_id, name='One member id per organization.'),
|
||||||
|
@ -134,6 +154,7 @@ class Person(Individual):
|
||||||
"""A person in the system. There can be several persons pointing to
|
"""A person in the system. There can be several persons pointing to
|
||||||
a real.
|
a real.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,37 +2,44 @@ from uuid import uuid4
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from sqlalchemy import Column, Boolean, BigInteger, Sequence
|
from sqlalchemy import BigInteger, Boolean, Column, Sequence
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy_utils import EmailType, PasswordType
|
from sqlalchemy_utils import EmailType, PasswordType
|
||||||
from teal.db import IntEnum
|
from teal.db import IntEnum
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.enums import SessionType
|
||||||
from ereuse_devicehub.resources.inventory.model import Inventory
|
from ereuse_devicehub.resources.inventory.model import Inventory
|
||||||
from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.enums import SessionType
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, Thing):
|
class User(UserMixin, Thing):
|
||||||
__table_args__ = {'schema': 'common'}
|
__table_args__ = {'schema': 'common'}
|
||||||
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
||||||
email = Column(EmailType, nullable=False, unique=True)
|
email = Column(EmailType, nullable=False, unique=True)
|
||||||
password = Column(PasswordType(max_length=STR_SIZE,
|
password = Column(
|
||||||
onload=lambda **kwargs: dict(
|
PasswordType(
|
||||||
schemes=app.config['PASSWORD_SCHEMES'],
|
max_length=STR_SIZE,
|
||||||
**kwargs
|
onload=lambda **kwargs: dict(
|
||||||
)))
|
schemes=app.config['PASSWORD_SCHEMES'], **kwargs
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||||
active = Column(Boolean, default=True, nullable=False)
|
active = Column(Boolean, default=True, nullable=False)
|
||||||
phantom = Column(Boolean, default=False, nullable=False)
|
phantom = Column(Boolean, default=False, nullable=False)
|
||||||
inventories = db.relationship(Inventory,
|
inventories = db.relationship(
|
||||||
backref=db.backref('users', lazy=True, collection_class=set),
|
Inventory,
|
||||||
secondary=lambda: UserInventory.__table__,
|
backref=db.backref('users', lazy=True, collection_class=set),
|
||||||
collection_class=set)
|
secondary=lambda: UserInventory.__table__,
|
||||||
|
collection_class=set,
|
||||||
|
)
|
||||||
|
|
||||||
# todo set restriction that user has, at least, one active db
|
# todo set restriction that user has, at least, one active db
|
||||||
|
|
||||||
def __init__(self, email, password=None, inventories=None, active=True, phantom=False) -> None:
|
def __init__(
|
||||||
|
self, email, password=None, inventories=None, active=True, phantom=False
|
||||||
|
) -> None:
|
||||||
"""Creates an user.
|
"""Creates an user.
|
||||||
:param email:
|
:param email:
|
||||||
:param password:
|
:param password:
|
||||||
|
@ -44,8 +51,13 @@ class User(UserMixin, Thing):
|
||||||
create during the trade actions
|
create during the trade actions
|
||||||
"""
|
"""
|
||||||
inventories = inventories or {Inventory.current}
|
inventories = inventories or {Inventory.current}
|
||||||
super().__init__(email=email, password=password, inventories=inventories,
|
super().__init__(
|
||||||
active=active, phantom=phantom)
|
email=email,
|
||||||
|
password=password,
|
||||||
|
inventories=inventories,
|
||||||
|
active=active,
|
||||||
|
phantom=phantom,
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<User {0.email}>'.format(self)
|
return '<User {0.email}>'.format(self)
|
||||||
|
@ -73,8 +85,8 @@ class User(UserMixin, Thing):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
# TODO(@slamora) create first_name & last_name fields and use
|
# TODO(@slamora) create first_name & last_name fields???
|
||||||
# them to generate user full name
|
# needs to be discussed related to Agent <--> User concepts
|
||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
|
@ -84,9 +96,12 @@ class User(UserMixin, Thing):
|
||||||
|
|
||||||
class UserInventory(db.Model):
|
class UserInventory(db.Model):
|
||||||
"""Relationship between users and their inventories."""
|
"""Relationship between users and their inventories."""
|
||||||
|
|
||||||
__table_args__ = {'schema': 'common'}
|
__table_args__ = {'schema': 'common'}
|
||||||
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True)
|
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True)
|
||||||
inventory_id = db.Column(db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True)
|
inventory_id = db.Column(
|
||||||
|
db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Session(Thing):
|
class Session(Thing):
|
||||||
|
@ -96,9 +111,11 @@ class Session(Thing):
|
||||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||||
type = Column(IntEnum(SessionType), default=SessionType.Internal, nullable=False)
|
type = Column(IntEnum(SessionType), default=SessionType.Internal, nullable=False)
|
||||||
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id))
|
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id))
|
||||||
user = db.relationship(User,
|
user = db.relationship(
|
||||||
backref=db.backref('sessions', lazy=True, collection_class=set),
|
User,
|
||||||
collection_class=set)
|
backref=db.backref('sessions', lazy=True, collection_class=set),
|
||||||
|
collection_class=set,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{0.token}'.format(self)
|
return '{0.token}'.format(self)
|
||||||
|
|
|
@ -194,39 +194,28 @@ async function processSelectedDevices() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage the actions that will be performed when applying the changes
|
* Manage the actions that will be performed when applying the changes
|
||||||
* @param {*} ev event (Should be a checkbox type)
|
* @param {EventSource} ev event (Should be a checkbox type)
|
||||||
* @param {string} lotID lot id
|
* @param {Lot} lot lot id
|
||||||
* @param {number} deviceID device id
|
* @param {Device[]} deviceList device id
|
||||||
*/
|
*/
|
||||||
manage(event, lotID, deviceListID) {
|
manage(event, lot, deviceListID) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const lotID = lot.id;
|
||||||
const srcElement = event.srcElement.parentElement.children[0]
|
const srcElement = event.srcElement.parentElement.children[0]
|
||||||
const {indeterminate} = srcElement;
|
|
||||||
const checked = !srcElement.checked;
|
const checked = !srcElement.checked;
|
||||||
|
|
||||||
const found = this.list.filter(list => list.lotID == lotID)[0];
|
const found = this.list.filter(list => list.lot.id == lotID)[0];
|
||||||
const foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1;
|
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (found != undefined && found.type == "Remove") {
|
if (found && found.type == "Remove") {
|
||||||
if (found.isFromIndeterminate == true) {
|
found.type = "Add";
|
||||||
found.type = "Add";
|
|
||||||
this.list[foundIndex] = found;
|
|
||||||
} else {
|
|
||||||
this.list = this.list.filter(list => list.lotID != lotID);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.list.push({ type: "Add", lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
|
this.list.push({ type: "Add", lot, devices: deviceListID});
|
||||||
}
|
}
|
||||||
} else if (found != undefined && found.type == "Add") {
|
} else if (found && found.type == "Add") {
|
||||||
if (found.isFromIndeterminate == true) {
|
found.type = "Remove";
|
||||||
found.type = "Remove";
|
|
||||||
this.list[foundIndex] = found;
|
|
||||||
} else {
|
|
||||||
this.list = this.list.filter(list => list.lotID != lotID);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.list.push({ type: "Remove", lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
|
this.list.push({ type: "Remove", lot, devices: deviceListID});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.list.length > 0) {
|
if (this.list.length > 0) {
|
||||||
|
@ -268,14 +257,14 @@ async function processSelectedDevices() {
|
||||||
this.list.forEach(async action => {
|
this.list.forEach(async action => {
|
||||||
if (action.type == "Add") {
|
if (action.type == "Add") {
|
||||||
try {
|
try {
|
||||||
await Api.devices_add(action.lotID, action.devices);
|
await Api.devices_add(action.lot.id, action.devices.map(dev => dev.data));
|
||||||
this.notifyUser("Devices sucefully aded to selected lot/s", "", false);
|
this.notifyUser("Devices sucefully aded to selected lot/s", "", false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
|
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
|
||||||
}
|
}
|
||||||
} else if (action.type == "Remove") {
|
} else if (action.type == "Remove") {
|
||||||
try {
|
try {
|
||||||
await Api.devices_remove(action.lotID, action.devices);
|
await Api.devices_remove(action.lot.id, action.devices.map(dev => dev.data));
|
||||||
this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
|
this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true);
|
this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true);
|
||||||
|
@ -299,15 +288,13 @@ async function processSelectedDevices() {
|
||||||
const tmpDiv = document.createElement("div")
|
const tmpDiv = document.createElement("div")
|
||||||
tmpDiv.innerHTML = newRequest
|
tmpDiv.innerHTML = newRequest
|
||||||
|
|
||||||
const oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
|
|
||||||
const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
|
const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
|
||||||
|
|
||||||
for (let i = 0; i < oldTable.length; i++) {
|
table.rows().dt.activeRows.forEach(row => {
|
||||||
if (!newTable.includes(oldTable[i])) {
|
if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) {
|
||||||
// variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411)
|
row.remove()
|
||||||
table.rows().remove(i)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,16 +332,22 @@ async function processSelectedDevices() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs));
|
doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices));
|
||||||
doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs));
|
doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices));
|
||||||
elementTarget.append(doc);
|
elementTarget.append(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
const listHTML = $("#LotsSelector")
|
const listHTML = $("#LotsSelector")
|
||||||
|
|
||||||
// Get selected devices
|
// Get selected devices
|
||||||
const selectedDevicesIDs = $.map($(".deviceSelect").filter(":checked"), (x) => parseInt($(x).attr("data")));
|
const selectedDevices = table.rows().dt.activeRows.filter(item => item.querySelector("input").checked).map(item => {
|
||||||
if (selectedDevicesIDs.length <= 0) {
|
const child = item.childNodes[0].children[0]
|
||||||
|
const info = {}
|
||||||
|
Object.values(child.attributes).forEach(attrib => { info[attrib.nodeName] = attrib.nodeValue })
|
||||||
|
return info
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selectedDevices.length <= 0) {
|
||||||
listHTML.html("<li style=\"color: red; text-align: center\">No devices selected</li>");
|
listHTML.html("<li style=\"color: red; text-align: center\">No devices selected</li>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -369,7 +362,7 @@ async function processSelectedDevices() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>")
|
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>")
|
||||||
const devices = await Api.get_devices(selectedDevicesIDs);
|
const devices = await Api.get_devices(selectedDevices.map(dev => dev.data));
|
||||||
let lots = await Api.get_lots();
|
let lots = await Api.get_lots();
|
||||||
|
|
||||||
lots = lots.map(lot => {
|
lots = lots.map(lot => {
|
||||||
|
@ -377,11 +370,11 @@ async function processSelectedDevices() {
|
||||||
.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0)
|
.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0)
|
||||||
.map(device => parseInt(device.id));
|
.map(device => parseInt(device.id));
|
||||||
|
|
||||||
switch (lot.devices.length) {
|
switch (lot.devices.length) {
|
||||||
case 0:
|
case 0:
|
||||||
lot.state = "false";
|
lot.state = "false";
|
||||||
break;
|
break;
|
||||||
case selectedDevicesIDs.length:
|
case selectedDevices.length:
|
||||||
lot.state = "true";
|
lot.state = "true";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -27,248 +27,40 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-8 d-none"><!-- TODO (hidden until is implemented )-->
|
<div class="col-xl-8">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
<!-- Bordered Tabs -->
|
<!-- Bordered Tabs -->
|
||||||
<ul class="nav nav-tabs nav-tabs-bordered">
|
<ul class="nav nav-tabs nav-tabs-bordered">
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#profile-overview">Overview</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-edit">Edit Profile</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-settings">Settings</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-change-password">Change Password</button>
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-change-password">Change Password</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content pt-2">
|
<div class="tab-content pt-2">
|
||||||
|
|
||||||
<div class="tab-pane fade show active profile-overview" id="profile-overview">
|
<div class="tab-pane fade show active pt-3" id="profile-change-password">
|
||||||
<h5 class="card-title">About</h5>
|
|
||||||
<p class="small fst-italic">Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.</p>
|
|
||||||
|
|
||||||
<h5 class="card-title">Profile Details</h5>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label ">Full Name</div>
|
|
||||||
<div class="col-lg-9 col-md-8">Kevin Anderson</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Company</div>
|
|
||||||
<div class="col-lg-9 col-md-8">Lueilwitz, Wisoky and Leuschke</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Job</div>
|
|
||||||
<div class="col-lg-9 col-md-8">Web Designer</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Country</div>
|
|
||||||
<div class="col-lg-9 col-md-8">USA</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Address</div>
|
|
||||||
<div class="col-lg-9 col-md-8">A108 Adam Street, New York, NY 535022</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Phone</div>
|
|
||||||
<div class="col-lg-9 col-md-8">(436) 486-3538 x29071</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Email</div>
|
|
||||||
<div class="col-lg-9 col-md-8">k.anderson@example.com</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade profile-edit pt-3" id="profile-edit">
|
|
||||||
|
|
||||||
<!-- Profile Edit Form -->
|
|
||||||
<form>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="profileImage" class="col-md-4 col-lg-3 col-form-label">Profile Image</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<img src="{{ url_for('static', filename='img/profile-img.jpg') }}" alt="Profile">
|
|
||||||
<div class="pt-2">
|
|
||||||
<a href="#" class="btn btn-primary btn-sm" title="Upload new profile image"><i class="bi bi-upload"></i></a>
|
|
||||||
<a href="#" class="btn btn-danger btn-sm" title="Remove my profile image"><i class="bi bi-trash"></i></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="fullName" class="col-md-4 col-lg-3 col-form-label">Full Name</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="fullName" type="text" class="form-control" id="fullName" value="Kevin Anderson">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="about" class="col-md-4 col-lg-3 col-form-label">About</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<textarea name="about" class="form-control" id="about" style="height: 100px">Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="company" class="col-md-4 col-lg-3 col-form-label">Company</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="company" type="text" class="form-control" id="company" value="Lueilwitz, Wisoky and Leuschke">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Job" class="col-md-4 col-lg-3 col-form-label">Job</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="job" type="text" class="form-control" id="Job" value="Web Designer">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Country" class="col-md-4 col-lg-3 col-form-label">Country</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="country" type="text" class="form-control" id="Country" value="USA">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Address" class="col-md-4 col-lg-3 col-form-label">Address</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="address" type="text" class="form-control" id="Address" value="A108 Adam Street, New York, NY 535022">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Phone" class="col-md-4 col-lg-3 col-form-label">Phone</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="phone" type="text" class="form-control" id="Phone" value="(436) 486-3538 x29071">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Email" class="col-md-4 col-lg-3 col-form-label">Email</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="email" type="email" class="form-control" id="Email" value="k.anderson@example.com">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Twitter" class="col-md-4 col-lg-3 col-form-label">Twitter Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="twitter" type="text" class="form-control" id="Twitter" value="https://twitter.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Facebook" class="col-md-4 col-lg-3 col-form-label">Facebook Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="facebook" type="text" class="form-control" id="Facebook" value="https://facebook.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Instagram" class="col-md-4 col-lg-3 col-form-label">Instagram Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="instagram" type="text" class="form-control" id="Instagram" value="https://instagram.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Linkedin" class="col-md-4 col-lg-3 col-form-label">Linkedin Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="linkedin" type="text" class="form-control" id="Linkedin" value="https://linkedin.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form><!-- End Profile Edit Form -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade pt-3" id="profile-settings">
|
|
||||||
|
|
||||||
<!-- Settings Form -->
|
|
||||||
<form>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="fullName" class="col-md-4 col-lg-3 col-form-label">Email Notifications</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="changesMade" checked>
|
|
||||||
<label class="form-check-label" for="changesMade">
|
|
||||||
Changes made to your account
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="newProducts" checked>
|
|
||||||
<label class="form-check-label" for="newProducts">
|
|
||||||
Information on new products and services
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="proOffers">
|
|
||||||
<label class="form-check-label" for="proOffers">
|
|
||||||
Marketing and promo offers
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="securityNotify" checked disabled>
|
|
||||||
<label class="form-check-label" for="securityNotify">
|
|
||||||
Security alerts
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form><!-- End settings Form -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade pt-3" id="profile-change-password">
|
|
||||||
<!-- Change Password Form -->
|
<!-- Change Password Form -->
|
||||||
<form>
|
<form action="{{ url_for('core.set-password') }}" method="post">
|
||||||
|
{% for f in password_form %}
|
||||||
|
{% if f == password_form.csrf_token %}
|
||||||
|
{{ f }}
|
||||||
|
{% else %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label for="currentPassword" class="col-md-4 col-lg-3 col-form-label">Current Password</label>
|
<label class="col-md-4 col-lg-3 col-form-label">{{ f.label }}</label>
|
||||||
<div class="col-md-8 col-lg-9">
|
<div class="col-md-8 col-lg-9">
|
||||||
<input name="password" type="password" class="form-control" id="currentPassword">
|
{{ f }}
|
||||||
|
{% if f.errors %}
|
||||||
|
<p class="text-danger">
|
||||||
|
{% for error in f.errors %}
|
||||||
|
{{ error }}<br/>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row mb-3">
|
{% endfor %}
|
||||||
<label for="newPassword" class="col-md-4 col-lg-3 col-form-label">New Password</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="newpassword" type="password" class="form-control" id="newPassword">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="renewPassword" class="col-md-4 col-lg-3 col-form-label">Re-enter New Password</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="renewpassword" type="password" class="form-control" id="renewPassword">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button type="submit" class="btn btn-primary">Change Password</button>
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,16 +37,20 @@
|
||||||
|
|
||||||
<div class="col-sm-12 col-md-7 d-md-flex justify-content-md-end"><!-- lot actions -->
|
<div class="col-sm-12 col-md-7 d-md-flex justify-content-md-end"><!-- lot actions -->
|
||||||
{% if lot.is_temporary %}
|
{% if lot.is_temporary %}
|
||||||
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
|
|
||||||
|
{% if 1 == 2 %}{# <!-- TODO (@slamora) Don't render Trade buttons until implementation is finished --> #}
|
||||||
<a class="me-2" href="javascript:newTrade('user_from')">
|
<a class="me-2" href="javascript:newTrade('user_from')">
|
||||||
<i class="bi bi-arrow-down-right"></i> Add supplier
|
<i class="bi bi-arrow-down-right"></i> Add supplier
|
||||||
</a>
|
</a>
|
||||||
<a class="me-2" href="javascript:newTrade('user_to')">
|
<a class="me-2" href="javascript:newTrade('user_to')">
|
||||||
<i class="bi bi-arrow-up-right"></i> Add receiver
|
<i class="bi bi-arrow-up-right"></i> Add receiver
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}{# <!-- /end TODO --> #}
|
||||||
|
|
||||||
<a class="text-danger" href="javascript:removeLot()">
|
<a class="text-danger" href="javascript:removeLot()">
|
||||||
<i class="bi bi-trash"></i> Delete Lot
|
<i class="bi bi-trash"></i> Delete Lot
|
||||||
</a>
|
</a>
|
||||||
|
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,8 +3,9 @@ from flask import Blueprint
|
||||||
from flask.views import View
|
from flask.views import View
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
|
|
||||||
from ereuse_devicehub import __version__
|
from ereuse_devicehub import __version__, messages
|
||||||
from ereuse_devicehub.forms import LoginForm
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.forms import LoginForm, PasswordForm
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from ereuse_devicehub.utils import is_safe_url
|
from ereuse_devicehub.utils import is_safe_url
|
||||||
|
|
||||||
|
@ -53,10 +54,30 @@ class UserProfileView(View):
|
||||||
context = {
|
context = {
|
||||||
'current_user': current_user,
|
'current_user': current_user,
|
||||||
'version': __version__,
|
'version': __version__,
|
||||||
|
'password_form': PasswordForm(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **context)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordView(View):
|
||||||
|
methods = ['POST']
|
||||||
|
decorators = [login_required]
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
form = PasswordForm()
|
||||||
|
db.session.commit()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.save(commit=False)
|
||||||
|
messages.success('Reset user password successfully!')
|
||||||
|
else:
|
||||||
|
messages.error('Error modifying user password!')
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return flask.redirect(flask.url_for('core.user-profile'))
|
||||||
|
|
||||||
|
|
||||||
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('/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'))
|
||||||
|
core.add_url_rule('/set_password/', view_func=UserPasswordView.as_view('set-password'))
|
||||||
|
|
Reference in New Issue