Merge branch 'testing' into change/#3202-change-tag-for-unique-identifier
This commit is contained in:
commit
c14d8482be
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
import pathlib
|
|
||||||
import copy
|
import copy
|
||||||
|
import pathlib
|
||||||
import time
|
import time
|
||||||
from flask import g
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set
|
||||||
from flask_sqlalchemy import event
|
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
|
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
|
||||||
|
from flask import g
|
||||||
|
from flask_sqlalchemy import event
|
||||||
from more_itertools import unique_everseen
|
from more_itertools import unique_everseen
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
from sqlalchemy import BigInteger, Boolean, Column
|
||||||
Sequence, SmallInteger, Unicode, inspect, text
|
from sqlalchemy import Enum as DBEnum
|
||||||
|
from sqlalchemy import (
|
||||||
|
Float,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
Sequence,
|
||||||
|
SmallInteger,
|
||||||
|
Unicode,
|
||||||
|
inspect,
|
||||||
|
text,
|
||||||
|
)
|
||||||
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.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
@ -22,19 +32,41 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
from sqlalchemy_utils import ColorType
|
from sqlalchemy_utils import ColorType
|
||||||
from stdnum import imei, meid
|
from stdnum import imei, meid
|
||||||
from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \
|
from teal.db import (
|
||||||
check_lower, check_range, IntEnum
|
CASCADE_DEL,
|
||||||
|
POLYMORPHIC_ID,
|
||||||
|
POLYMORPHIC_ON,
|
||||||
|
URL,
|
||||||
|
IntEnum,
|
||||||
|
ResourceNotFound,
|
||||||
|
check_lower,
|
||||||
|
check_range,
|
||||||
|
)
|
||||||
from teal.enums import Layouts
|
from teal.enums import Layouts
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import url_for_resource
|
from teal.resource import url_for_resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.utils import hashcode
|
|
||||||
from ereuse_devicehub.resources.enums import BatteryTechnology, CameraFacing, ComputerChassis, \
|
|
||||||
DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity, TransferState
|
|
||||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing, listener_reset_field_updated_in_actual_time
|
|
||||||
from ereuse_devicehub.resources.user.models import User
|
|
||||||
from ereuse_devicehub.resources.device.metrics import Metrics
|
from ereuse_devicehub.resources.device.metrics import Metrics
|
||||||
|
from ereuse_devicehub.resources.enums import (
|
||||||
|
BatteryTechnology,
|
||||||
|
CameraFacing,
|
||||||
|
ComputerChassis,
|
||||||
|
DataStorageInterface,
|
||||||
|
DisplayTech,
|
||||||
|
PrinterTechnology,
|
||||||
|
RamFormat,
|
||||||
|
RamInterface,
|
||||||
|
Severity,
|
||||||
|
TransferState,
|
||||||
|
)
|
||||||
|
from ereuse_devicehub.resources.models import (
|
||||||
|
STR_SM_SIZE,
|
||||||
|
Thing,
|
||||||
|
listener_reset_field_updated_in_actual_time,
|
||||||
|
)
|
||||||
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
from ereuse_devicehub.resources.utils import hashcode
|
||||||
|
|
||||||
|
|
||||||
def create_code(context):
|
def create_code(context):
|
||||||
|
@ -58,17 +90,21 @@ class Device(Thing):
|
||||||
Devices can contain ``Components``, which are just a type of device
|
Devices can contain ``Components``, which are just a type of device
|
||||||
(it is a recursive relationship).
|
(it is a recursive relationship).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
||||||
id.comment = """The identifier of the device for this database. Used only
|
id.comment = """The identifier of the device for this database. Used only
|
||||||
internally for software; users should not use this.
|
internally for software; users should not use this.
|
||||||
"""
|
"""
|
||||||
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||||
hid = Column(Unicode(), check_lower('hid'), unique=False)
|
hid = Column(Unicode(), check_lower('hid'), unique=False)
|
||||||
hid.comment = """The Hardware ID (HID) is the ID traceability
|
hid.comment = (
|
||||||
|
"""The Hardware ID (HID) is the ID traceability
|
||||||
systems use to ID a device globally. This field is auto-generated
|
systems use to ID a device globally. This field is auto-generated
|
||||||
from Devicehub using literal identifiers from the device,
|
from Devicehub using literal identifiers from the device,
|
||||||
so it can re-generated *offline*.
|
so it can re-generated *offline*.
|
||||||
""" + HID_CONVERSION_DOC
|
"""
|
||||||
|
+ HID_CONVERSION_DOC
|
||||||
|
)
|
||||||
model = Column(Unicode(), check_lower('model'))
|
model = Column(Unicode(), check_lower('model'))
|
||||||
model.comment = """The model of the device in lower case.
|
model.comment = """The model of the device in lower case.
|
||||||
|
|
||||||
|
@ -118,14 +154,18 @@ class Device(Thing):
|
||||||
image = db.Column(db.URL)
|
image = db.Column(db.URL)
|
||||||
image.comment = "An image of the device."
|
image.comment = "An image of the device."
|
||||||
|
|
||||||
owner_id = db.Column(UUID(as_uuid=True),
|
owner_id = db.Column(
|
||||||
db.ForeignKey(User.id),
|
UUID(as_uuid=True),
|
||||||
nullable=False,
|
db.ForeignKey(User.id),
|
||||||
default=lambda: g.user.id)
|
nullable=False,
|
||||||
|
default=lambda: g.user.id,
|
||||||
|
)
|
||||||
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
||||||
allocated = db.Column(Boolean, default=False)
|
allocated = db.Column(Boolean, default=False)
|
||||||
allocated.comment = "device is allocated or not."
|
allocated.comment = "device is allocated or not."
|
||||||
devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code)
|
devicehub_id = db.Column(
|
||||||
|
db.CIText(), nullable=True, unique=True, default=create_code
|
||||||
|
)
|
||||||
devicehub_id.comment = "device have a unique code."
|
devicehub_id.comment = "device have a unique code."
|
||||||
active = db.Column(Boolean, default=True)
|
active = db.Column(Boolean, default=True)
|
||||||
|
|
||||||
|
@ -152,12 +192,12 @@ class Device(Thing):
|
||||||
'image',
|
'image',
|
||||||
'allocated',
|
'allocated',
|
||||||
'devicehub_id',
|
'devicehub_id',
|
||||||
'active'
|
'active',
|
||||||
}
|
}
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.Index('device_id', id, postgresql_using='hash'),
|
db.Index('device_id', id, postgresql_using='hash'),
|
||||||
db.Index('type_index', type, postgresql_using='hash')
|
db.Index('type_index', type, postgresql_using='hash'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, **kw) -> None:
|
def __init__(self, **kw) -> None:
|
||||||
|
@ -187,7 +227,9 @@ class Device(Thing):
|
||||||
for ac in actions_one:
|
for ac in actions_one:
|
||||||
ac.real_created = ac.created
|
ac.real_created = ac.created
|
||||||
|
|
||||||
return sorted(chain(actions_multiple, actions_one), key=lambda x: x.real_created)
|
return sorted(
|
||||||
|
chain(actions_multiple, actions_one), key=lambda x: x.real_created
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def problems(self):
|
def problems(self):
|
||||||
|
@ -196,8 +238,9 @@ class Device(Thing):
|
||||||
There can be up to 3 actions: current Snapshot,
|
There can be up to 3 actions: current Snapshot,
|
||||||
current Physical action, current Trading action.
|
current Physical action, current Trading action.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.device import states
|
|
||||||
from ereuse_devicehub.resources.action.models import Snapshot
|
from ereuse_devicehub.resources.action.models import Snapshot
|
||||||
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
actions = set()
|
actions = set()
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
actions.add(self.last_action_of(Snapshot))
|
actions.add(self.last_action_of(Snapshot))
|
||||||
|
@ -217,11 +260,13 @@ class Device(Thing):
|
||||||
"""
|
"""
|
||||||
# todo ensure to remove materialized values when start using them
|
# todo ensure to remove materialized values when start using them
|
||||||
# todo or self.__table__.columns if inspect fails
|
# todo or self.__table__.columns if inspect fails
|
||||||
return {c.key: getattr(self, c.key, None)
|
return {
|
||||||
for c in inspect(self.__class__).attrs
|
c.key: getattr(self, c.key, None)
|
||||||
if isinstance(c, ColumnProperty)
|
for c in inspect(self.__class__).attrs
|
||||||
and not getattr(c, 'foreign_keys', None)
|
if isinstance(c, ColumnProperty)
|
||||||
and c.key not in self._NON_PHYSICAL_PROPS}
|
and not getattr(c, 'foreign_keys', None)
|
||||||
|
and c.key not in self._NON_PHYSICAL_PROPS
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_properties(self) -> Dict[str, object or None]:
|
def public_properties(self) -> Dict[str, object or None]:
|
||||||
|
@ -234,11 +279,13 @@ class Device(Thing):
|
||||||
"""
|
"""
|
||||||
non_public = ['amount', 'transfer_state', 'receiver_id']
|
non_public = ['amount', 'transfer_state', 'receiver_id']
|
||||||
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
|
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
|
||||||
return {c.key: getattr(self, c.key, None)
|
return {
|
||||||
for c in inspect(self.__class__).attrs
|
c.key: getattr(self, c.key, None)
|
||||||
if isinstance(c, ColumnProperty)
|
for c in inspect(self.__class__).attrs
|
||||||
and not getattr(c, 'foreign_keys', None)
|
if isinstance(c, ColumnProperty)
|
||||||
and c.key not in hide_properties}
|
and not getattr(c, 'foreign_keys', None)
|
||||||
|
and c.key not in hide_properties
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_actions(self) -> List[object]:
|
def public_actions(self) -> List[object]:
|
||||||
|
@ -260,6 +307,7 @@ class Device(Thing):
|
||||||
"""The last Rate of the device."""
|
"""The last Rate of the device."""
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
from ereuse_devicehub.resources.action.models import Rate
|
from ereuse_devicehub.resources.action.models import Rate
|
||||||
|
|
||||||
return self.last_action_of(Rate)
|
return self.last_action_of(Rate)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -268,12 +316,14 @@ class Device(Thing):
|
||||||
ever been set."""
|
ever been set."""
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
from ereuse_devicehub.resources.action.models import Price
|
from ereuse_devicehub.resources.action.models import Price
|
||||||
|
|
||||||
return self.last_action_of(Price)
|
return self.last_action_of(Price)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_action_trading(self):
|
def last_action_trading(self):
|
||||||
"""which is the last action trading"""
|
"""which is the last action trading"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
return self.last_action_of(*states.Trading.actions())
|
return self.last_action_of(*states.Trading.actions())
|
||||||
|
|
||||||
|
@ -287,6 +337,7 @@ class Device(Thing):
|
||||||
- Management
|
- Management
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
return self.last_action_of(*states.Status.actions())
|
return self.last_action_of(*states.Status.actions())
|
||||||
|
|
||||||
|
@ -300,6 +351,7 @@ class Device(Thing):
|
||||||
- Management
|
- Management
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
status_actions = [ac.t for ac in states.Status.actions()]
|
status_actions = [ac.t for ac in states.Status.actions()]
|
||||||
history = []
|
history = []
|
||||||
for ac in self.actions:
|
for ac in self.actions:
|
||||||
|
@ -329,13 +381,15 @@ class Device(Thing):
|
||||||
if not hasattr(lot, 'trade'):
|
if not hasattr(lot, 'trade'):
|
||||||
return
|
return
|
||||||
|
|
||||||
Status = {0: 'Trade',
|
Status = {
|
||||||
1: 'Confirm',
|
0: 'Trade',
|
||||||
2: 'NeedConfirmation',
|
1: 'Confirm',
|
||||||
3: 'TradeConfirmed',
|
2: 'NeedConfirmation',
|
||||||
4: 'Revoke',
|
3: 'TradeConfirmed',
|
||||||
5: 'NeedConfirmRevoke',
|
4: 'Revoke',
|
||||||
6: 'RevokeConfirmed'}
|
5: 'NeedConfirmRevoke',
|
||||||
|
6: 'RevokeConfirmed',
|
||||||
|
}
|
||||||
|
|
||||||
trade = lot.trade
|
trade = lot.trade
|
||||||
user_from = trade.user_from
|
user_from = trade.user_from
|
||||||
|
@ -408,6 +462,7 @@ class Device(Thing):
|
||||||
"""If the actual trading state is an revoke action, this property show
|
"""If the actual trading state is an revoke action, this property show
|
||||||
the id of that revoke"""
|
the id of that revoke"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Trading.actions())
|
action = self.last_action_of(*states.Trading.actions())
|
||||||
if action.type == 'Revoke':
|
if action.type == 'Revoke':
|
||||||
|
@ -417,6 +472,7 @@ class Device(Thing):
|
||||||
def physical(self):
|
def physical(self):
|
||||||
"""The actual physical state, None otherwise."""
|
"""The actual physical state, None otherwise."""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Physical.actions())
|
action = self.last_action_of(*states.Physical.actions())
|
||||||
return states.Physical(action.__class__)
|
return states.Physical(action.__class__)
|
||||||
|
@ -425,6 +481,7 @@ class Device(Thing):
|
||||||
def traking(self):
|
def traking(self):
|
||||||
"""The actual traking state, None otherwise."""
|
"""The actual traking state, None otherwise."""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Traking.actions())
|
action = self.last_action_of(*states.Traking.actions())
|
||||||
return states.Traking(action.__class__)
|
return states.Traking(action.__class__)
|
||||||
|
@ -433,6 +490,7 @@ class Device(Thing):
|
||||||
def usage(self):
|
def usage(self):
|
||||||
"""The actual usage state, None otherwise."""
|
"""The actual usage state, None otherwise."""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Usage.actions())
|
action = self.last_action_of(*states.Usage.actions())
|
||||||
return states.Usage(action.__class__)
|
return states.Usage(action.__class__)
|
||||||
|
@ -470,8 +528,11 @@ class Device(Thing):
|
||||||
test has been executed.
|
test has been executed.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.action.models import Test
|
from ereuse_devicehub.resources.action.models import Test
|
||||||
current_tests = unique_everseen((e for e in reversed(self.actions) if isinstance(e, Test)),
|
|
||||||
key=attrgetter('type')) # last test of each type
|
current_tests = unique_everseen(
|
||||||
|
(e for e in reversed(self.actions) if isinstance(e, Test)),
|
||||||
|
key=attrgetter('type'),
|
||||||
|
) # last test of each type
|
||||||
return self._warning_actions(current_tests)
|
return self._warning_actions(current_tests)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -496,7 +557,9 @@ class Device(Thing):
|
||||||
|
|
||||||
def set_hid(self):
|
def set_hid(self):
|
||||||
with suppress(TypeError):
|
with suppress(TypeError):
|
||||||
self.hid = Naming.hid(self.type, self.manufacturer, self.model, self.serial_number)
|
self.hid = Naming.hid(
|
||||||
|
self.type, self.manufacturer, self.model, self.serial_number
|
||||||
|
)
|
||||||
|
|
||||||
def last_action_of(self, *types):
|
def last_action_of(self, *types):
|
||||||
"""Gets the last action of the given types.
|
"""Gets the last action of the given types.
|
||||||
|
@ -509,7 +572,9 @@ class Device(Thing):
|
||||||
actions.sort(key=lambda x: x.created)
|
actions.sort(key=lambda x: x.created)
|
||||||
return next(e for e in reversed(actions) if isinstance(e, types))
|
return next(e for e in reversed(actions) if isinstance(e, types))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise LookupError('{!r} does not contain actions of types {}.'.format(self, types))
|
raise LookupError(
|
||||||
|
'{!r} does not contain actions of types {}.'.format(self, types)
|
||||||
|
)
|
||||||
|
|
||||||
def which_user_put_this_device_in_trace(self):
|
def which_user_put_this_device_in_trace(self):
|
||||||
"""which is the user than put this device in this trade"""
|
"""which is the user than put this device in this trade"""
|
||||||
|
@ -546,6 +611,32 @@ class Device(Thing):
|
||||||
metrics = Metrics(device=self)
|
metrics = Metrics(device=self)
|
||||||
return metrics.get_metrics()
|
return metrics.get_metrics()
|
||||||
|
|
||||||
|
def get_type_logo(self):
|
||||||
|
# This is used for see one logo of type of device in the frontend
|
||||||
|
types = {
|
||||||
|
"Desktop": "bi bi-file-post-fill",
|
||||||
|
"Laptop": "bi bi-laptop",
|
||||||
|
"Server": "bi bi-server",
|
||||||
|
"Processor": "bi bi-cpu",
|
||||||
|
"RamModule": "bi bi-list",
|
||||||
|
"Motherboard": "bi bi-cpu-fill",
|
||||||
|
"NetworkAdapter": "bi bi-hdd-network",
|
||||||
|
"GraphicCard": "bi bi-brush",
|
||||||
|
"SoundCard": "bi bi-volume-up-fill",
|
||||||
|
"Monitor": "bi bi-display",
|
||||||
|
"Display": "bi bi-display",
|
||||||
|
"ComputerMonitor": "bi bi-display",
|
||||||
|
"TelevisionSet": "bi bi-easel",
|
||||||
|
"TV": "bi bi-easel",
|
||||||
|
"Projector": "bi bi-camera-video",
|
||||||
|
"Tablet": "bi bi-tablet-landscape",
|
||||||
|
"Smartphone": "bi bi-phone",
|
||||||
|
"Cellphone": "bi bi-telephone",
|
||||||
|
"HardDrive": "bi bi-hdd-stack",
|
||||||
|
"SolidStateDrive": "bi bi-hdd",
|
||||||
|
}
|
||||||
|
return types.get(self.type, '')
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.id < other.id
|
return self.id < other.id
|
||||||
|
|
||||||
|
@ -571,19 +662,24 @@ class Device(Thing):
|
||||||
|
|
||||||
class DisplayMixin:
|
class DisplayMixin:
|
||||||
"""Base class for the Display Component and the Monitor Device."""
|
"""Base class for the Display Component and the Monitor Device."""
|
||||||
size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True)
|
|
||||||
|
size = Column(
|
||||||
|
Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True
|
||||||
|
)
|
||||||
size.comment = """The size of the monitor in inches."""
|
size.comment = """The size of the monitor in inches."""
|
||||||
technology = Column(DBEnum(DisplayTech))
|
technology = Column(DBEnum(DisplayTech))
|
||||||
technology.comment = """The technology the monitor uses to display
|
technology.comment = """The technology the monitor uses to display
|
||||||
the image.
|
the image.
|
||||||
"""
|
"""
|
||||||
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000),
|
resolution_width = Column(
|
||||||
nullable=True)
|
SmallInteger, check_range('resolution_width', 10, 20000), nullable=True
|
||||||
|
)
|
||||||
resolution_width.comment = """The maximum horizontal resolution the
|
resolution_width.comment = """The maximum horizontal resolution the
|
||||||
monitor can natively support in pixels.
|
monitor can natively support in pixels.
|
||||||
"""
|
"""
|
||||||
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000),
|
resolution_height = Column(
|
||||||
nullable=True)
|
SmallInteger, check_range('resolution_height', 10, 20000), nullable=True
|
||||||
|
)
|
||||||
resolution_height.comment = """The maximum vertical resolution the
|
resolution_height.comment = """The maximum vertical resolution the
|
||||||
monitor can natively support in pixels.
|
monitor can natively support in pixels.
|
||||||
"""
|
"""
|
||||||
|
@ -622,8 +718,12 @@ class DisplayMixin:
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if self.size:
|
if self.size:
|
||||||
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self)
|
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(
|
||||||
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(self)
|
self
|
||||||
|
)
|
||||||
|
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(
|
||||||
|
self
|
||||||
|
)
|
||||||
|
|
||||||
def __format__(self, format_spec: str) -> str:
|
def __format__(self, format_spec: str) -> str:
|
||||||
v = ''
|
v = ''
|
||||||
|
@ -645,6 +745,7 @@ class Computer(Device):
|
||||||
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
||||||
``Server``. The property ``chassis`` defines it more granularly.
|
``Server``. The property ``chassis`` defines it more granularly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
chassis = Column(DBEnum(ComputerChassis), nullable=True)
|
chassis = Column(DBEnum(ComputerChassis), nullable=True)
|
||||||
chassis.comment = """The physical form of the computer.
|
chassis.comment = """The physical form of the computer.
|
||||||
|
@ -652,16 +753,18 @@ class Computer(Device):
|
||||||
It is a subset of the Linux definition of DMI / DMI decode.
|
It is a subset of the Linux definition of DMI / DMI decode.
|
||||||
"""
|
"""
|
||||||
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
||||||
owner_id = db.Column(UUID(as_uuid=True),
|
owner_id = db.Column(
|
||||||
db.ForeignKey(User.id),
|
UUID(as_uuid=True),
|
||||||
nullable=False,
|
db.ForeignKey(User.id),
|
||||||
default=lambda: g.user.id)
|
nullable=False,
|
||||||
|
default=lambda: g.user.id,
|
||||||
|
)
|
||||||
# author = db.relationship(User, primaryjoin=owner_id == User.id)
|
# author = db.relationship(User, primaryjoin=owner_id == User.id)
|
||||||
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False)
|
transfer_state = db.Column(
|
||||||
|
IntEnum(TransferState), default=TransferState.Initial, nullable=False
|
||||||
|
)
|
||||||
transfer_state.comment = TransferState.__doc__
|
transfer_state.comment = TransferState.__doc__
|
||||||
receiver_id = db.Column(UUID(as_uuid=True),
|
receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
|
||||||
db.ForeignKey(User.id),
|
|
||||||
nullable=True)
|
|
||||||
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
|
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
@ -684,22 +787,30 @@ class Computer(Device):
|
||||||
@property
|
@property
|
||||||
def ram_size(self) -> int:
|
def ram_size(self) -> int:
|
||||||
"""The total of RAM memory the computer has."""
|
"""The total of RAM memory the computer has."""
|
||||||
return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule))
|
return sum(
|
||||||
|
ram.size or 0 for ram in self.components if isinstance(ram, RamModule)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data_storage_size(self) -> int:
|
def data_storage_size(self) -> int:
|
||||||
"""The total of data storage the computer has."""
|
"""The total of data storage the computer has."""
|
||||||
return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage))
|
return sum(
|
||||||
|
ds.size or 0 for ds in self.components if isinstance(ds, DataStorage)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def processor_model(self) -> str:
|
def processor_model(self) -> str:
|
||||||
"""The model of one of the processors of the computer."""
|
"""The model of one of the processors of the computer."""
|
||||||
return next((p.model for p in self.components if isinstance(p, Processor)), None)
|
return next(
|
||||||
|
(p.model for p in self.components if isinstance(p, Processor)), None
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def graphic_card_model(self) -> str:
|
def graphic_card_model(self) -> str:
|
||||||
"""The model of one of the graphic cards of the computer."""
|
"""The model of one of the graphic cards of the computer."""
|
||||||
return next((p.model for p in self.components if isinstance(p, GraphicCard)), None)
|
return next(
|
||||||
|
(p.model for p in self.components if isinstance(p, GraphicCard)), None
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def network_speeds(self) -> List[int]:
|
def network_speeds(self) -> List[int]:
|
||||||
|
@ -724,16 +835,18 @@ class Computer(Device):
|
||||||
it is not None.
|
it is not None.
|
||||||
"""
|
"""
|
||||||
return set(
|
return set(
|
||||||
privacy for privacy in
|
privacy
|
||||||
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage))
|
for privacy in (
|
||||||
|
hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage)
|
||||||
|
)
|
||||||
if privacy
|
if privacy
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def external_document_erasure(self):
|
def external_document_erasure(self):
|
||||||
"""Returns the external ``DataStorage`` proof of erasure.
|
"""Returns the external ``DataStorage`` proof of erasure."""
|
||||||
"""
|
|
||||||
from ereuse_devicehub.resources.action.models import DataWipe
|
from ereuse_devicehub.resources.action.models import DataWipe
|
||||||
|
|
||||||
urls = set()
|
urls = set()
|
||||||
try:
|
try:
|
||||||
ev = self.last_action_of(DataWipe)
|
ev = self.last_action_of(DataWipe)
|
||||||
|
@ -756,8 +869,11 @@ class Computer(Device):
|
||||||
if not self.hid:
|
if not self.hid:
|
||||||
return
|
return
|
||||||
components = self.components if components_snap is None else components_snap
|
components = self.components if components_snap is None else components_snap
|
||||||
macs_network = [c.serial_number for c in components
|
macs_network = [
|
||||||
if c.type == 'NetworkAdapter' and c.serial_number is not None]
|
c.serial_number
|
||||||
|
for c in components
|
||||||
|
if c.type == 'NetworkAdapter' and c.serial_number is not None
|
||||||
|
]
|
||||||
macs_network.sort()
|
macs_network.sort()
|
||||||
mac = macs_network[0] if macs_network else ''
|
mac = macs_network[0] if macs_network else ''
|
||||||
if not mac or mac in self.hid:
|
if not mac or mac in self.hid:
|
||||||
|
@ -823,9 +939,13 @@ class Mobile(Device):
|
||||||
"""
|
"""
|
||||||
ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000))
|
ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000))
|
||||||
ram_size.comment = """The total of RAM of the device in MB."""
|
ram_size.comment = """The total of RAM of the device in MB."""
|
||||||
data_storage_size = db.Column(db.Integer, check_range('data_storage_size', 0, 10 ** 8))
|
data_storage_size = db.Column(
|
||||||
|
db.Integer, check_range('data_storage_size', 0, 10**8)
|
||||||
|
)
|
||||||
data_storage_size.comment = """The total of data storage of the device in MB"""
|
data_storage_size.comment = """The total of data storage of the device in MB"""
|
||||||
display_size = db.Column(db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0))
|
display_size = db.Column(
|
||||||
|
db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0)
|
||||||
|
)
|
||||||
display_size.comment = """The total size of the device screen"""
|
display_size.comment = """The total size of the device screen"""
|
||||||
|
|
||||||
@validates('imei')
|
@validates('imei')
|
||||||
|
@ -855,21 +975,24 @@ class Cellphone(Mobile):
|
||||||
|
|
||||||
class Component(Device):
|
class Component(Device):
|
||||||
"""A device that can be inside another device."""
|
"""A device that can be inside another device."""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||||
parent = relationship(Computer,
|
parent = relationship(
|
||||||
backref=backref('components',
|
Computer,
|
||||||
lazy=True,
|
backref=backref(
|
||||||
cascade=CASCADE_DEL,
|
'components',
|
||||||
order_by=lambda: Component.id,
|
lazy=True,
|
||||||
collection_class=OrderedSet),
|
cascade=CASCADE_DEL,
|
||||||
primaryjoin=parent_id == Computer.id)
|
order_by=lambda: Component.id,
|
||||||
|
collection_class=OrderedSet,
|
||||||
__table_args__ = (
|
),
|
||||||
db.Index('parent_index', parent_id, postgresql_using='hash'),
|
primaryjoin=parent_id == Computer.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__table_args__ = (db.Index('parent_index', parent_id, postgresql_using='hash'),)
|
||||||
|
|
||||||
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
||||||
"""Gets a component that:
|
"""Gets a component that:
|
||||||
|
|
||||||
|
@ -881,11 +1004,16 @@ class Component(Device):
|
||||||
when looking for similar ones.
|
when looking for similar ones.
|
||||||
"""
|
"""
|
||||||
assert self.hid is None, 'Don\'t use this method with a component that has HID'
|
assert self.hid is None, 'Don\'t use this method with a component that has HID'
|
||||||
component = self.__class__.query \
|
component = (
|
||||||
.filter_by(parent=parent, hid=None, owner_id=self.owner_id,
|
self.__class__.query.filter_by(
|
||||||
**self.physical_properties) \
|
parent=parent,
|
||||||
.filter(~Component.id.in_(blacklist)) \
|
hid=None,
|
||||||
|
owner_id=self.owner_id,
|
||||||
|
**self.physical_properties,
|
||||||
|
)
|
||||||
|
.filter(~Component.id.in_(blacklist))
|
||||||
.first()
|
.first()
|
||||||
|
)
|
||||||
if not component:
|
if not component:
|
||||||
raise ResourceNotFound(self.type)
|
raise ResourceNotFound(self.type)
|
||||||
return component
|
return component
|
||||||
|
@ -908,7 +1036,8 @@ class GraphicCard(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
class DataStorage(JoinedComponentTableMixin, Component):
|
class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
"""A device that stores information."""
|
"""A device that stores information."""
|
||||||
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
|
|
||||||
|
size = Column(Integer, check_range('size', min=1, max=10**8))
|
||||||
size.comment = """The size of the data-storage in MB."""
|
size.comment = """The size of the data-storage in MB."""
|
||||||
interface = Column(DBEnum(DataStorageInterface))
|
interface = Column(DBEnum(DataStorageInterface))
|
||||||
|
|
||||||
|
@ -919,6 +1048,7 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
This is, the last erasure performed to the data storage.
|
This is, the last erasure performed to the data storage.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.action.models import EraseBasic
|
from ereuse_devicehub.resources.action.models import EraseBasic
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ev = self.last_action_of(EraseBasic)
|
ev = self.last_action_of(EraseBasic)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
|
@ -933,9 +1063,9 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def external_document_erasure(self):
|
def external_document_erasure(self):
|
||||||
"""Returns the external ``DataStorage`` proof of erasure.
|
"""Returns the external ``DataStorage`` proof of erasure."""
|
||||||
"""
|
|
||||||
from ereuse_devicehub.resources.action.models import DataWipe
|
from ereuse_devicehub.resources.action.models import DataWipe
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ev = self.last_action_of(DataWipe)
|
ev = self.last_action_of(DataWipe)
|
||||||
return ev.document.url.to_text()
|
return ev.document.url.to_text()
|
||||||
|
@ -985,6 +1115,7 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
|
||||||
|
|
||||||
class Processor(JoinedComponentTableMixin, Component):
|
class Processor(JoinedComponentTableMixin, Component):
|
||||||
"""The CPU."""
|
"""The CPU."""
|
||||||
|
|
||||||
speed = Column(Float, check_range('speed', 0.1, 15))
|
speed = Column(Float, check_range('speed', 0.1, 15))
|
||||||
speed.comment = """The regular CPU speed."""
|
speed.comment = """The regular CPU speed."""
|
||||||
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
||||||
|
@ -999,6 +1130,7 @@ class Processor(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
class RamModule(JoinedComponentTableMixin, Component):
|
class RamModule(JoinedComponentTableMixin, Component):
|
||||||
"""A stick of RAM."""
|
"""A stick of RAM."""
|
||||||
|
|
||||||
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
||||||
size.comment = """The capacity of the RAM stick."""
|
size.comment = """The capacity of the RAM stick."""
|
||||||
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
|
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
|
||||||
|
@ -1016,6 +1148,7 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component):
|
||||||
mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
|
mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
|
||||||
and ``TelevisionSet``.
|
and ``TelevisionSet``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1031,14 +1164,16 @@ class Battery(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capacity(self) -> float:
|
def capacity(self) -> float:
|
||||||
"""The quantity of """
|
"""The quantity of"""
|
||||||
from ereuse_devicehub.resources.action.models import MeasureBattery
|
from ereuse_devicehub.resources.action.models import MeasureBattery
|
||||||
|
|
||||||
real_size = self.last_action_of(MeasureBattery).size
|
real_size = self.last_action_of(MeasureBattery).size
|
||||||
return real_size / self.size if real_size and self.size else None
|
return real_size / self.size if real_size and self.size else None
|
||||||
|
|
||||||
|
|
||||||
class Camera(Component):
|
class Camera(Component):
|
||||||
"""The camera of a device."""
|
"""The camera of a device."""
|
||||||
|
|
||||||
focal_length = db.Column(db.SmallInteger)
|
focal_length = db.Column(db.SmallInteger)
|
||||||
video_height = db.Column(db.SmallInteger)
|
video_height = db.Column(db.SmallInteger)
|
||||||
video_width = db.Column(db.Integer)
|
video_width = db.Column(db.Integer)
|
||||||
|
@ -1051,6 +1186,7 @@ class Camera(Component):
|
||||||
|
|
||||||
class ComputerAccessory(Device):
|
class ComputerAccessory(Device):
|
||||||
"""Computer peripherals and similar accessories."""
|
"""Computer peripherals and similar accessories."""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1073,6 +1209,7 @@ class MemoryCardReader(ComputerAccessory):
|
||||||
|
|
||||||
class Networking(NetworkMixin, Device):
|
class Networking(NetworkMixin, Device):
|
||||||
"""Routers, switches, hubs..."""
|
"""Routers, switches, hubs..."""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1118,6 +1255,7 @@ class Microphone(Sound):
|
||||||
|
|
||||||
class Video(Device):
|
class Video(Device):
|
||||||
"""Devices related to video treatment."""
|
"""Devices related to video treatment."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1131,6 +1269,7 @@ class Videoconference(Video):
|
||||||
|
|
||||||
class Cooking(Device):
|
class Cooking(Device):
|
||||||
"""Cooking devices."""
|
"""Cooking devices."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1182,6 +1321,7 @@ class Manufacturer(db.Model):
|
||||||
Ideally users should use the names from this list when submitting
|
Ideally users should use the names from this list when submitting
|
||||||
devices.
|
devices.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = db.Column(CIText(), primary_key=True)
|
name = db.Column(CIText(), primary_key=True)
|
||||||
name.comment = """The normalized name of the manufacturer."""
|
name.comment = """The normalized name of the manufacturer."""
|
||||||
url = db.Column(URL(), unique=True)
|
url = db.Column(URL(), unique=True)
|
||||||
|
@ -1192,7 +1332,7 @@ class Manufacturer(db.Model):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
||||||
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
|
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
|
||||||
{'schema': 'common'}
|
{'schema': 'common'},
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1202,10 +1342,7 @@ class Manufacturer(db.Model):
|
||||||
#: Dialect used to write the CSV
|
#: Dialect used to write the CSV
|
||||||
|
|
||||||
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
|
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
|
||||||
cursor.copy_expert(
|
cursor.copy_expert('COPY common.manufacturer FROM STDIN (FORMAT csv)', f)
|
||||||
'COPY common.manufacturer FROM STDIN (FORMAT csv)',
|
|
||||||
f
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
listener_reset_field_updated_in_actual_time(Device)
|
listener_reset_field_updated_in_actual_time(Device)
|
||||||
|
@ -1217,6 +1354,7 @@ def create_code_tag(mapper, connection, device):
|
||||||
this tag is the same of devicehub_id.
|
this tag is the same of devicehub_id.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
|
||||||
if isinstance(device, Computer):
|
if isinstance(device, Computer):
|
||||||
tag = Tag(device_id=device.id, id=device.devicehub_id)
|
tag = Tag(device_id=device.id, id=device.devicehub_id)
|
||||||
db.session.add(tag)
|
db.session.add(tag)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -349,6 +353,9 @@
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if dev.get_type_logo() %}
|
||||||
|
<i class="{{ dev.get_type_logo() }}" title="{{ dev.type }}"></i>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
|
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
|
||||||
{{ dev.verbose_name }}
|
{{ dev.verbose_name }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -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