Merge branch 'testing' into feature/server-side-render-testing

This commit is contained in:
Cayo Puigdefabregas 2022-04-29 13:12:09 +02:00
commit ef71eed27f
31 changed files with 1057 additions and 638 deletions

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
ereuse_devicehub/static/vendor
ereuse_devicehub/static/js/print.pdf.js
ereuse_devicehub/static/js/qrcode.js
*.min.js

36
.eslintrc.json Normal file
View File

@ -0,0 +1,36 @@
{
"env": {
"browser": true,
"es2021": true,
"jquery": true
},
"extends": [
"airbnb",
"prettier"
],
"plugins": [
"prettier"
],
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
"quotes": ["error","double"],
"no-use-before-define": "off",
"no-unused-vars": "warn",
"no-undef": "warn",
"camelcase": "off",
"no-console": "off",
"no-plusplus": "off",
"no-param-reassign": "off",
"no-new": "warn",
"strict": "off",
"class-methods-use-this": "off",
"eqeqeq": "warn",
"radix": "warn"
},
"globals": {
"API_URLS": true,
"Api": true
}
}

55
.github/workflows/eslint.yml vendored Normal file
View File

@ -0,0 +1,55 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# ESLint is a tool for identifying and reporting on patterns
# found in ECMAScript/JavaScript code.
# More details at https://github.com/eslint/eslint
# and https://eslint.org
name: ESLint
on:
push:
branches: [master, testing]
pull_request_target:
branches: [master, testing]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
prettier: false
commit_message: "Fix code style issues with ${linter}"
auto_fix: true
commit: true
github_token: "${{ secrets.GITHUB_TOKEN }}"
git_name: "Lint Action"
- name: Save Code Linting Report JSON
# npm script for ESLint
# eslint --output-file eslint_report.json --format json src
# See https://eslint.org/docs/user-guide/command-line-interface#options
run: npm run lint:report
# Continue to the next step even if this fails
continue-on-error: true
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@1.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
report-json: "eslint_report.json"
only-pr-files: true
- name: Upload ESLint report
uses: actions/upload-artifact@v2
with:
name: eslint_report.json
path: eslint_report.json

8
.gitignore vendored
View File

@ -119,3 +119,11 @@ ENV/
# Temporal dir # Temporal dir
tmp/ tmp/
# NPM modules
node_modules/
yarn.lock
# ESLint Report
eslint_report.json

3
.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 250
}

View File

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

View File

@ -64,10 +64,7 @@ class PrintLabelsForm(FlaskForm):
.all() .all()
) )
# print only tags that are DHID if not self._devices:
dhids = [x.devicehub_id for x in self._devices] return False
self._tags = (
Tag.query.filter(Tag.owner_id == g.user.id).filter(Tag.id.in_(dhids)).all()
)
return is_valid return is_valid

View File

@ -27,7 +27,7 @@ class TagListView(View):
context = { context = {
'lots': lots, 'lots': lots,
'tags': tags, 'tags': tags,
'page_title': 'Tags Management', 'page_title': 'Unique Identifiers Management',
'version': __version__, 'version': __version__,
} }
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
@ -102,7 +102,7 @@ class PrintLabelsView(View):
form = PrintLabelsForm() form = PrintLabelsForm()
if form.validate_on_submit(): if form.validate_on_submit():
context['form'] = form context['form'] = form
context['tags'] = form._tags context['devices'] = form._devices
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
else: else:
messages.error('Error you need select one or more devices') messages.error('Error you need select one or more devices')

View File

@ -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
@ -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,7 +80,8 @@ class Agent(Thing):
class Organization(JoinedTableMixin, Agent): class Organization(JoinedTableMixin, Agent):
default_of = db.relationship(Inventory, default_of = db.relationship(
Inventory,
uselist=False, uselist=False,
lazy=True, lazy=True,
backref=backref('org', lazy=True), backref=backref('org', lazy=True),
@ -84,7 +89,8 @@ class Organization(JoinedTableMixin, Agent):
# as foreign keys can only reference to one table # as foreign keys can only reference to one table
# and we have multiple organization table (one per schema) # and we have multiple organization table (one per schema)
foreign_keys=[Inventory.org_id], foreign_keys=[Inventory.org_id],
primaryjoin=lambda: Organization.id == 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(
User,
backref=backref('individuals', lazy=True, collection_class=set), backref=backref('individuals', lazy=True, collection_class=set),
primaryjoin=user_id == User.id) 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

View File

@ -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(
UUID(as_uuid=True),
db.ForeignKey(User.id), db.ForeignKey(User.id),
nullable=False, nullable=False,
default=lambda: g.user.id) 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 {
c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty) if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None) and not getattr(c, 'foreign_keys', None)
and c.key not in self._NON_PHYSICAL_PROPS} 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 {
c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty) if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None) and not getattr(c, 'foreign_keys', None)
and c.key not in hide_properties} 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 = {
0: 'Trade',
1: 'Confirm', 1: 'Confirm',
2: 'NeedConfirmation', 2: 'NeedConfirmation',
3: 'TradeConfirmed', 3: 'TradeConfirmed',
4: 'Revoke', 4: 'Revoke',
5: 'NeedConfirmRevoke', 5: 'NeedConfirmRevoke',
6: 'RevokeConfirmed'} 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(
UUID(as_uuid=True),
db.ForeignKey(User.id), db.ForeignKey(User.id),
nullable=False, nullable=False,
default=lambda: g.user.id) 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,
backref=backref(
'components',
lazy=True, lazy=True,
cascade=CASCADE_DEL, cascade=CASCADE_DEL,
order_by=lambda: Component.id, order_by=lambda: Component.id,
collection_class=OrderedSet), collection_class=OrderedSet,
primaryjoin=parent_id == Computer.id) ),
primaryjoin=parent_id == Computer.id,
__table_args__ = (
db.Index('parent_index', parent_id, postgresql_using='hash'),
) )
__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,6 +1036,7 @@ 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
@ -1033,12 +1166,14 @@ class Battery(JoinedComponentTableMixin, Component):
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)

View File

@ -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(
PasswordType(
max_length=STR_SIZE,
onload=lambda **kwargs: dict( onload=lambda **kwargs: dict(
schemes=app.config['PASSWORD_SCHEMES'], schemes=app.config['PASSWORD_SCHEMES'], **kwargs
**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(
Inventory,
backref=db.backref('users', lazy=True, collection_class=set), backref=db.backref('users', lazy=True, collection_class=set),
secondary=lambda: UserInventory.__table__, secondary=lambda: UserInventory.__table__,
collection_class=set) 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(
User,
backref=db.backref('sessions', lazy=True, collection_class=set), backref=db.backref('sessions', lazy=True, collection_class=set),
collection_class=set) collection_class=set,
)
def __str__(self) -> str: def __str__(self) -> str:
return '{0.token}'.format(self) return '{0.token}'.format(self)

View File

@ -0,0 +1,25 @@
/**
* eReuse CSS
*/
/*--------------------------------------------------------------
# LotsSelector
--------------------------------------------------------------*/
#dropDownLotsSelector {
max-height: 500px;
}
#dropDownLotsSelector>ul#LotsSelector {
list-style-type: none;
margin: 0;
padding: 0;
min-width: max-content;
max-height: 380px;
overflow-y: auto;
}
#dropDownLotsSelector #ApplyDeviceLots {
padding-top: 0px;
padding-bottom: 5px;
}

View File

@ -4,7 +4,7 @@ const Api = {
* @returns get lots * @returns get lots
*/ */
async get_lots() { async get_lots() {
var request = await this.doRequest(API_URLS.lots, "GET", null); const request = await this.doRequest(API_URLS.lots, "GET", null);
if (request != undefined) return request.items; if (request != undefined) return request.items;
throw request; throw request;
}, },
@ -15,7 +15,7 @@ const Api = {
* @returns full detailed device list * @returns full detailed device list
*/ */
async get_devices(ids) { async get_devices(ids) {
var request = await this.doRequest(API_URLS.devices + '?filter={"id": [' + ids.toString() + ']}', "GET", null); const request = await this.doRequest(`${API_URLS.devices }?filter={"id": [${ ids.toString() }]}`, "GET", null);
if (request != undefined) return request.items; if (request != undefined) return request.items;
throw request; throw request;
}, },
@ -26,7 +26,7 @@ const Api = {
* @returns full detailed device list * @returns full detailed device list
*/ */
async search_device(id) { async search_device(id) {
var request = await this.doRequest(API_URLS.devices + '?filter={"devicehub_id": ["' + id + '"]}', "GET", null) const request = await this.doRequest(`${API_URLS.devices }?filter={"devicehub_id": ["${ id }"]}`, "GET", null)
if (request != undefined) return request.items if (request != undefined) return request.items
throw request throw request
}, },
@ -37,8 +37,8 @@ const Api = {
* @param {number[]} listDevices list devices id * @param {number[]} listDevices list devices id
*/ */
async devices_add(lotID, listDevices) { async devices_add(lotID, listDevices) {
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&"); const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`;
return await Api.doRequest(queryURL, "POST", null); return Api.doRequest(queryURL, "POST", null);
}, },
/** /**
@ -47,8 +47,8 @@ const Api = {
* @param {number[]} listDevices list devices id * @param {number[]} listDevices list devices id
*/ */
async devices_remove(lotID, listDevices) { async devices_remove(lotID, listDevices) {
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&"); const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`;
return await Api.doRequest(queryURL, "DELETE", null); return Api.doRequest(queryURL, "DELETE", null);
}, },
/** /**
@ -59,13 +59,13 @@ const Api = {
* @returns * @returns
*/ */
async doRequest(url, type, body) { async doRequest(url, type, body) {
var result; let result;
try { try {
result = await $.ajax({ result = await $.ajax({
url: url, url,
type: type, type,
headers: { "Authorization": API_URLS.Auth_Token }, headers: { "Authorization": API_URLS.Auth_Token },
body: body body
}); });
return result; return result;
} catch (error) { } catch (error) {

View File

@ -1,15 +1,15 @@
$(document).ready(function() { $(document).ready(() => {
$("#type").on("change", deviceInputs); $("#type").on("change", deviceInputs);
deviceInputs(); deviceInputs();
}) })
function deviceInputs() { function deviceInputs() {
if ($("#type").val() == 'Monitor') { if ($("#type").val() == "Monitor") {
$("#screen").show(); $("#screen").show();
$("#resolution").show(); $("#resolution").show();
$("#imei").hide(); $("#imei").hide();
$("#meid").hide(); $("#meid").hide();
} else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) { } else if (["Smartphone", "Cellphone", "Tablet"].includes($("#type").val())) {
$("#screen").hide(); $("#screen").hide();
$("#resolution").hide(); $("#resolution").hide();
$("#imei").show(); $("#imei").show();

View File

@ -14,9 +14,9 @@
el = el.trim() el = el.trim()
if (all) { if (all) {
return [...document.querySelectorAll(el)] return [...document.querySelectorAll(el)]
} else {
return document.querySelector(el)
} }
return document.querySelector(el)
} }
/** /**
@ -34,103 +34,101 @@
* Easy on scroll event listener * Easy on scroll event listener
*/ */
const onscroll = (el, listener) => { const onscroll = (el, listener) => {
el.addEventListener('scroll', listener) el.addEventListener("scroll", listener)
} }
/** /**
* Sidebar toggle * Sidebar toggle
*/ */
if (select('.toggle-sidebar-btn')) { if (select(".toggle-sidebar-btn")) {
on('click', '.toggle-sidebar-btn', function (e) { on("click", ".toggle-sidebar-btn", (e) => {
select('body').classList.toggle('toggle-sidebar') select("body").classList.toggle("toggle-sidebar")
}) })
} }
/** /**
* Search bar toggle * Search bar toggle
*/ */
if (select('.search-bar-toggle')) { if (select(".search-bar-toggle")) {
on('click', '.search-bar-toggle', function (e) { on("click", ".search-bar-toggle", (e) => {
select('.search-bar').classList.toggle('search-bar-show') select(".search-bar").classList.toggle("search-bar-show")
}) })
} }
/** /**
* Navbar links active state on scroll * Navbar links active state on scroll
*/ */
let navbarlinks = select('#navbar .scrollto', true) const navbarlinks = select("#navbar .scrollto", true)
const navbarlinksActive = () => { const navbarlinksActive = () => {
let position = window.scrollY + 200 const position = window.scrollY + 200
navbarlinks.forEach(navbarlink => { navbarlinks.forEach(navbarlink => {
if (!navbarlink.hash) return if (!navbarlink.hash) return
let section = select(navbarlink.hash) const section = select(navbarlink.hash)
if (!section) return if (!section) return
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) { if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
navbarlink.classList.add('active') navbarlink.classList.add("active")
} else { } else {
navbarlink.classList.remove('active') navbarlink.classList.remove("active")
} }
}) })
} }
window.addEventListener('load', navbarlinksActive) window.addEventListener("load", navbarlinksActive)
onscroll(document, navbarlinksActive) onscroll(document, navbarlinksActive)
/** /**
* Toggle .header-scrolled class to #header when page is scrolled * Toggle .header-scrolled class to #header when page is scrolled
*/ */
let selectHeader = select('#header') const selectHeader = select("#header")
if (selectHeader) { if (selectHeader) {
const headerScrolled = () => { const headerScrolled = () => {
if (window.scrollY > 100) { if (window.scrollY > 100) {
selectHeader.classList.add('header-scrolled') selectHeader.classList.add("header-scrolled")
} else { } else {
selectHeader.classList.remove('header-scrolled') selectHeader.classList.remove("header-scrolled")
} }
} }
window.addEventListener('load', headerScrolled) window.addEventListener("load", headerScrolled)
onscroll(document, headerScrolled) onscroll(document, headerScrolled)
} }
/** /**
* Back to top button * Back to top button
*/ */
let backtotop = select('.back-to-top') const backtotop = select(".back-to-top")
if (backtotop) { if (backtotop) {
const toggleBacktotop = () => { const toggleBacktotop = () => {
if (window.scrollY > 100) { if (window.scrollY > 100) {
backtotop.classList.add('active') backtotop.classList.add("active")
} else { } else {
backtotop.classList.remove('active') backtotop.classList.remove("active")
} }
} }
window.addEventListener('load', toggleBacktotop) window.addEventListener("load", toggleBacktotop)
onscroll(document, toggleBacktotop) onscroll(document, toggleBacktotop)
} }
/** /**
* Initiate tooltips * Initiate tooltips
*/ */
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl))
return new bootstrap.Tooltip(tooltipTriggerEl)
})
/** /**
* Initiate quill editors * Initiate quill editors
*/ */
if (select('.quill-editor-default')) { if (select(".quill-editor-default")) {
new Quill('.quill-editor-default', { new Quill(".quill-editor-default", {
theme: 'snow' theme: "snow"
}); });
} }
if (select('.quill-editor-bubble')) { if (select(".quill-editor-bubble")) {
new Quill('.quill-editor-bubble', { new Quill(".quill-editor-bubble", {
theme: 'bubble' theme: "bubble"
}); });
} }
if (select('.quill-editor-full')) { if (select(".quill-editor-full")) {
new Quill(".quill-editor-full", { new Quill(".quill-editor-full", {
modules: { modules: {
toolbar: [ toolbar: [
@ -181,24 +179,24 @@
/** /**
* Initiate Bootstrap validation check * Initiate Bootstrap validation check
*/ */
var needsValidation = document.querySelectorAll('.needs-validation') const needsValidation = document.querySelectorAll(".needs-validation")
Array.prototype.slice.call(needsValidation) Array.prototype.slice.call(needsValidation)
.forEach(function (form) { .forEach((form) => {
form.addEventListener('submit', function (event) { form.addEventListener("submit", (event) => {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
} }
form.classList.add('was-validated') form.classList.add("was-validated")
}, false) }, false)
}) })
/** /**
* Initiate Datatables * Initiate Datatables
*/ */
const datatables = select('.datatable', true) const datatables = select(".datatable", true)
datatables.forEach(datatable => { datatables.forEach(datatable => {
new simpleDatatables.DataTable(datatable); new simpleDatatables.DataTable(datatable);
}) })
@ -206,11 +204,11 @@
/** /**
* Autoresize echart charts * Autoresize echart charts
*/ */
const mainContainer = select('#main'); const mainContainer = select("#main");
if (mainContainer) { if (mainContainer) {
setTimeout(() => { setTimeout(() => {
new ResizeObserver(function () { new ResizeObserver(() => {
select('.echart', true).forEach(getEchart => { select(".echart", true).forEach(getEchart => {
echarts.getInstanceByDom(getEchart).resize(); echarts.getInstanceByDom(getEchart).resize();
}) })
}).observe(mainContainer); }).observe(mainContainer);
@ -220,11 +218,11 @@
/** /**
* Select all functionality * Select all functionality
*/ */
var btnSelectAll = document.getElementById("SelectAllBTN"); const btnSelectAll = document.getElementById("SelectAllBTN");
var tableListCheckboxes = document.querySelectorAll(".deviceSelect"); const tableListCheckboxes = document.querySelectorAll(".deviceSelect");
function itemListCheckChanged(event) { function itemListCheckChanged(event) {
let isAllChecked = Array.from(tableListCheckboxes).map(itm => itm.checked); const isAllChecked = Array.from(tableListCheckboxes).map(itm => itm.checked);
if (isAllChecked.every(bool => bool == true)) { if (isAllChecked.every(bool => bool == true)) {
btnSelectAll.checked = true; btnSelectAll.checked = true;
btnSelectAll.indeterminate = false; btnSelectAll.indeterminate = false;
@ -241,8 +239,8 @@
}) })
btnSelectAll.addEventListener("click", event => { btnSelectAll.addEventListener("click", event => {
let checkedState = event.target.checked; const checkedState = event.target.checked;
tableListCheckboxes.forEach(ckeckbox => ckeckbox.checked = checkedState); tableListCheckboxes.forEach(ckeckbox => {ckeckbox.checked = checkedState});
}) })
/** /**
@ -256,23 +254,23 @@
* Search form functionality * Search form functionality
*/ */
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
var searchForm = document.getElementById("SearchForm") const searchForm = document.getElementById("SearchForm")
var inputSearch = document.querySelector("#SearchForm > input") const inputSearch = document.querySelector("#SearchForm > input")
var doSearch = true const doSearch = true
searchForm.addEventListener("submit", (event) => { searchForm.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
}) })
let timeoutHandler = setTimeout(() => { }, 1) let timeoutHandler = setTimeout(() => { }, 1)
let dropdownList = document.getElementById("dropdown-search-list") const dropdownList = document.getElementById("dropdown-search-list")
let defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
inputSearch.addEventListener("input", (e) => { inputSearch.addEventListener("input", (e) => {
clearTimeout(timeoutHandler) clearTimeout(timeoutHandler)
let searchText = e.target.value const searchText = e.target.value
if (searchText == '') { if (searchText == "") {
document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch; document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
return return
} }
@ -315,7 +313,7 @@
const device = devices[i]; const device = devices[i];
// See: ereuse_devicehub/resources/device/models.py // See: ereuse_devicehub/resources/device/models.py
var verboseName = `${device.type} ${device.manufacturer} ${device.model}` const verboseName = `${device.type} ${device.manufacturer} ${device.model}`
const templateString = ` const templateString = `
<li> <li>

View File

@ -1,7 +1,7 @@
$(document).ready(function() { $(document).ready(() => {
var show_allocate_form = $("#allocateModal").data('show-action-form'); const show_allocate_form = $("#allocateModal").data("show-action-form");
var show_datawipe_form = $("#datawipeModal").data('show-action-form'); const show_datawipe_form = $("#datawipeModal").data("show-action-form");
var show_trade_form = $("#tradeLotModal").data('show-action-form'); const show_trade_form = $("#tradeLotModal").data("show-action-form");
if (show_allocate_form != "None") { if (show_allocate_form != "None") {
$("#allocateModal .btn-primary").show(); $("#allocateModal .btn-primary").show();
newAllocate(show_allocate_form); newAllocate(show_allocate_form);
@ -18,7 +18,7 @@ $(document).ready(function() {
}) })
function deviceSelect() { function deviceSelect() {
var devices_count = $(".deviceSelect").filter(':checked').length; const devices_count = $(".deviceSelect").filter(":checked").length;
get_device_list(); get_device_list();
if (devices_count == 0) { if (devices_count == 0) {
$("#addingLotModal .pol").show(); $("#addingLotModal .pol").show();
@ -60,7 +60,7 @@ function deviceSelect() {
} }
function removeLot() { function removeLot() {
var devices = $(".deviceSelect"); const devices = $(".deviceSelect");
if (devices.length > 0) { if (devices.length > 0) {
$("#btnRemoveLots .text-danger").show(); $("#btnRemoveLots .text-danger").show();
} else { } else {
@ -70,10 +70,10 @@ function removeLot() {
} }
function removeTag() { function removeTag() {
var devices = $(".deviceSelect").filter(':checked'); const devices = $(".deviceSelect").filter(":checked");
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); const devices_id = $.map(devices, (x) => $(x).attr("data"));
if (devices_id.length == 1) { if (devices_id.length == 1) {
var url = "/inventory/tag/devices/"+devices_id[0]+"/del/"; const url = `/inventory/tag/devices/${devices_id[0]}/del/`;
window.location.href = url; window.location.href = url;
} else { } else {
$("#unlinkTagAlertModal").click(); $("#unlinkTagAlertModal").click();
@ -81,8 +81,8 @@ function removeTag() {
} }
function addTag() { function addTag() {
var devices = $(".deviceSelect").filter(':checked'); const devices = $(".deviceSelect").filter(":checked");
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); const devices_id = $.map(devices, (x) => $(x).attr("data"));
if (devices_id.length == 1) { if (devices_id.length == 1) {
$("#addingTagModal .pol").hide(); $("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show(); $("#addingTagModal .btn-primary").show();
@ -95,20 +95,20 @@ function addTag() {
} }
function newTrade(action) { function newTrade(action) {
var title = "Trade " let title = "Trade "
var user_to = $("#user_to").data("email"); const user_to = $("#user_to").data("email");
var user_from = $("#user_from").data("email"); const user_from = $("#user_from").data("email");
if (action == 'user_from') { if (action == "user_from") {
title = 'Trade Incoming'; title = "Trade Incoming";
$("#user_to").attr('readonly', 'readonly'); $("#user_to").attr("readonly", "readonly");
$("#user_from").prop('readonly', false); $("#user_from").prop("readonly", false);
$("#user_from").val(''); $("#user_from").val("");
$("#user_to").val(user_to); $("#user_to").val(user_to);
} else if (action == 'user_to') { } else if (action == "user_to") {
title = 'Trade Outgoing'; title = "Trade Outgoing";
$("#user_from").attr('readonly', 'readonly'); $("#user_from").attr("readonly", "readonly");
$("#user_to").prop('readonly', false); $("#user_to").prop("readonly", false);
$("#user_to").val(''); $("#user_to").val("");
$("#user_from").val(user_from); $("#user_from").val(user_from);
} }
$("#tradeLotModal #title-action").html(title); $("#tradeLotModal #title-action").html(title);
@ -137,44 +137,44 @@ function newDataWipe(action) {
} }
function get_device_list() { function get_device_list() {
var devices = $(".deviceSelect").filter(':checked'); const devices = $(".deviceSelect").filter(":checked");
/* Insert the correct count of devices in actions form */ /* Insert the correct count of devices in actions form */
var devices_count = devices.length; const devices_count = devices.length;
$("#datawipeModal .devices-count").html(devices_count); $("#datawipeModal .devices-count").html(devices_count);
$("#allocateModal .devices-count").html(devices_count); $("#allocateModal .devices-count").html(devices_count);
$("#actionModal .devices-count").html(devices_count); $("#actionModal .devices-count").html(devices_count);
/* Insert the correct value in the input devicesList */ /* Insert the correct value in the input devicesList */
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(","); const devices_id = $.map(devices, (x) => $(x).attr("data")).join(",");
$.map($(".devicesList"), function(x) { $.map($(".devicesList"), (x) => {
$(x).val(devices_id); $(x).val(devices_id);
}); });
/* Create a list of devices for human representation */ /* Create a list of devices for human representation */
var computer = { const computer = {
"Desktop": "<i class='bi bi-building'></i>", "Desktop": "<i class='bi bi-building'></i>",
"Laptop": "<i class='bi bi-laptop'></i>", "Laptop": "<i class='bi bi-laptop'></i>",
}; };
list_devices = devices.map(function (x) { list_devices = devices.map((x) => {
var typ = $(devices[x]).data("device-type"); let typ = $(devices[x]).data("device-type");
var manuf = $(devices[x]).data("device-manufacturer"); const manuf = $(devices[x]).data("device-manufacturer");
var dhid = $(devices[x]).data("device-dhid"); const dhid = $(devices[x]).data("device-dhid");
if (computer[typ]) { if (computer[typ]) {
typ = computer[typ]; typ = computer[typ];
}; };
return typ + " " + manuf + " " + dhid; return `${typ } ${ manuf } ${ dhid}`;
}); });
description = $.map(list_devices, function(x) { return x }).join(", "); description = $.map(list_devices, (x) => x).join(", ");
$(".enumeration-devices").html(description); $(".enumeration-devices").html(description);
} }
function export_file(type_file) { function export_file(type_file) {
var devices = $(".deviceSelect").filter(':checked'); const devices = $(".deviceSelect").filter(":checked");
var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(","); const devices_id = $.map(devices, (x) => $(x).attr("data-device-dhid")).join(",");
if (devices_id){ if (devices_id){
var url = "/inventory/export/"+type_file+"/?ids="+devices_id; const url = `/inventory/export/${type_file}/?ids=${devices_id}`;
window.location.href = url; window.location.href = url;
} else { } else {
$("#exportAlertModal").click(); $("#exportAlertModal").click();
@ -200,11 +200,12 @@ async function processSelectedDevices() {
*/ */
manage(event, lotID, deviceListID) { manage(event, lotID, deviceListID) {
event.preventDefault(); event.preventDefault();
const indeterminate = event.srcElement.indeterminate; const srcElement = event.srcElement.parentElement.children[0]
const checked = !event.srcElement.checked; const {indeterminate} = srcElement;
const checked = !srcElement.checked;
var found = this.list.filter(list => list.lotID == lotID)[0]; const found = this.list.filter(list => list.lotID == lotID)[0];
var foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1; 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 != undefined && found.type == "Remove") {
@ -215,10 +216,9 @@ async function processSelectedDevices() {
this.list = this.list.filter(list => list.lotID != lotID); this.list = this.list.filter(list => list.lotID != lotID);
} }
} else { } else {
this.list.push({ type: "Add", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); this.list.push({ type: "Add", lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
} }
} else { } else if (found != undefined && found.type == "Add") {
if (found != undefined && found.type == "Add") {
if (found.isFromIndeterminate == true) { if (found.isFromIndeterminate == true) {
found.type = "Remove"; found.type = "Remove";
this.list[foundIndex] = found; this.list[foundIndex] = found;
@ -226,8 +226,7 @@ async function processSelectedDevices() {
this.list = this.list.filter(list => list.lotID != lotID); this.list = this.list.filter(list => list.lotID != lotID);
} }
} else { } else {
this.list.push({ type: "Remove", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); this.list.push({ type: "Remove", lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
}
} }
if (this.list.length > 0) { if (this.list.length > 0) {
@ -244,10 +243,10 @@ async function processSelectedDevices() {
* @param {boolean} isError defines if a toast is a error * @param {boolean} isError defines if a toast is a error
*/ */
notifyUser(title, toastText, isError) { notifyUser(title, toastText, isError) {
let toast = document.createElement("div"); const toast = document.createElement("div");
toast.classList = "alert alert-dismissible fade show " + (isError ? "alert-danger" : "alert-success"); toast.classList = `alert alert-dismissible fade show ${ isError ? "alert-danger" : "alert-success"}`;
toast.attributes["data-autohide"] = !isError; toast.attributes["data-autohide"] = !isError;
toast.attributes["role"] = "alert"; toast.attributes.role = "alert";
toast.style = "margin-left: auto; width: fit-content;"; toast.style = "margin-left: auto; width: fit-content;";
toast.innerHTML = `<strong>${title}</strong><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`; toast.innerHTML = `<strong>${title}</strong><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
if (toastText && toastText.length > 0) { if (toastText && toastText.length > 0) {
@ -265,7 +264,7 @@ async function processSelectedDevices() {
* Get actions and execute call request to add or remove devices from lots * Get actions and execute call request to add or remove devices from lots
*/ */
doActions() { doActions() {
var requestCount = 0; // This is for count all requested api count, to perform reRender of table device list let requestCount = 0; // This is for count all requested api count, to perform reRender of table device list
this.list.forEach(async action => { this.list.forEach(async action => {
if (action.type == "Add") { if (action.type == "Add") {
try { try {
@ -295,13 +294,13 @@ async function processSelectedDevices() {
* Re-render list in table * Re-render list in table
*/ */
async reRenderTable() { async reRenderTable() {
var newRequest = await Api.doRequest(window.location) const newRequest = await Api.doRequest(window.location)
var tmpDiv = document.createElement("div") const tmpDiv = document.createElement("div")
tmpDiv.innerHTML = newRequest tmpDiv.innerHTML = newRequest
var oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) const oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
var 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++) { for (let i = 0; i < oldTable.length; i++) {
if (!newTable.includes(oldTable[i])) { if (!newTable.includes(oldTable[i])) {
@ -312,7 +311,7 @@ async function processSelectedDevices() {
} }
} }
var eventClickActions; let eventClickActions;
/** /**
* Generates a list item with a correspondient checkbox state * Generates a list item with a correspondient checkbox state
@ -321,42 +320,47 @@ async function processSelectedDevices() {
* @param {Array<number>} selectedDevicesIDs * @param {Array<number>} selectedDevicesIDs
* @param {HTMLElement} target * @param {HTMLElement} target
*/ */
function templateLot(lotID, lot, selectedDevicesIDs, elementTarget, actions) { function templateLot(lot, elementTarget, actions) {
elementTarget.innerHTML = "" elementTarget.innerHTML = ""
const {id, name, state} = lot;
var htmlTemplate = `<input class="form-check-input" type="checkbox" id="${lotID}" style="width: 20px; height: 20px; margin-right: 7px;"> const htmlTemplate = `<input class="form-check-input" type="checkbox" id="${id}" style="width: 20px; height: 20px; margin-right: 7px;">
<label class="form-check-label" for="${lotID}">${lot.name}</label>`; <label class="form-check-label" for="${id}">${name}</label>`;
var existLotList = selectedDevicesIDs.map(selected => lot.devices.includes(selected)); const doc = document.createElement("li");
var doc = document.createElement('li');
doc.innerHTML = htmlTemplate; doc.innerHTML = htmlTemplate;
if (selectedDevicesIDs.length <= 0) { switch (state) {
doc.children[0].disabled = true; case "true":
} else if (existLotList.every(value => value == true)) {
doc.children[0].checked = true; doc.children[0].checked = true;
} else if (existLotList.every(value => value == false)) { break;
case "false":
doc.children[0].checked = false; doc.children[0].checked = false;
} else { break;
case "indetermined":
doc.children[0].indeterminate = true; doc.children[0].indeterminate = true;
break;
default:
console.warn("This shouldn't be happend: Lot without state: ", lot);
break;
} }
doc.children[0].addEventListener('mouseup', (ev) => actions.manage(ev, lotID, selectedDevicesIDs)); doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs));
doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs));
elementTarget.append(doc); elementTarget.append(doc);
} }
var listHTML = $("#LotsSelector") const listHTML = $("#LotsSelector")
// Get selected devices // Get selected devices
var selectedDevicesIDs = $.map($(".deviceSelect").filter(':checked'), function (x) { return parseInt($(x).attr('data')) }); const selectedDevicesIDs = $.map($(".deviceSelect").filter(":checked"), (x) => parseInt($(x).attr("data")));
if (selectedDevicesIDs.length <= 0) { if (selectedDevicesIDs.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;
} }
// Initialize Actions list, and set checkbox triggers // Initialize Actions list, and set checkbox triggers
var actions = new Actions(); const actions = new Actions();
if (eventClickActions) { if (eventClickActions) {
document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions); document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions);
} }
@ -364,21 +368,41 @@ async function processSelectedDevices() {
document.getElementById("ApplyDeviceLots").classList.add("disabled"); document.getElementById("ApplyDeviceLots").classList.add("disabled");
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>")
var devices = await Api.get_devices(selectedDevicesIDs); const devices = await Api.get_devices(selectedDevicesIDs);
var lots = await Api.get_lots(); let lots = await Api.get_lots();
lots = lots.map(lot => { lots = lots.map(lot => {
lot.devices = devices lot.devices = devices
.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) {
case 0:
lot.state = "false";
break;
case selectedDevicesIDs.length:
lot.state = "true";
break;
default:
lot.state = "indetermined";
break;
}
return lot; return lot;
}) })
listHTML.html('');
lots.forEach(lot => templateLot(lot.id, lot, selectedDevicesIDs, listHTML, actions)); let lotsList = [];
lotsList.push(lots.filter(lot => lot.state == "true").sort((a,b) => a.name.localeCompare(b.name)));
lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a,b) => a.name.localeCompare(b.name)));
lotsList.push(lots.filter(lot => lot.state == "false").sort((a,b) => a.name.localeCompare(b.name)));
lotsList = lotsList.flat(); // flat array
listHTML.html("");
lotsList.forEach(lot => templateLot(lot, listHTML, actions));
} catch (error) { } catch (error) {
console.log(error); console.log(error);
listHTML.html('<li style="color: red; text-align: center">Error feching devices and lots<br>(see console for more details)</li>'); listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");
} }
} }

View File

@ -1,8 +1,10 @@
$(document).ready(function() { $(document).ready(function() {
STORAGE_KEY = 'tag-spec-key'; STORAGE_KEY = 'tag-spec-key';
$("#printerType").on("change", change_size); $("#printerType").on("change", change_size);
$(".form-check-input").on("change", change_check);
change_size(); change_size();
load_size(); load_settings();
change_check();
}) })
function qr_draw(url, id) { function qr_draw(url, id) {
@ -16,27 +18,43 @@ function qr_draw(url, id) {
}); });
} }
function save_size() { function save_settings() {
var height = $("#height-tag").val(); var height = $("#height-tag").val();
var width = $("#width-tag").val(); var width = $("#width-tag").val();
var sizePreset = $("#printerType").val(); var sizePreset = $("#printerType").val();
var data = {"height": height, "width": width, "sizePreset": sizePreset}; var data = {"height": height, "width": width, "sizePreset": sizePreset};
data['dhid'] = $("#dhidCheck").prop('checked');
data['qr'] = $("#qrCheck").prop('checked');
data['serial_number'] = $("#serialNumberCheck").prop('checked');
data['manufacturer'] = $("#manufacturerCheck").prop('checked');
data['model'] = $("#modelCheck").prop('checked');
localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} }
function load_size() { function load_settings() {
var data = JSON.parse(localStorage.getItem(STORAGE_KEY)); var data = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (data){ if (data){
$("#height-tag").val(data.height); $("#height-tag").val(data.height);
$("#width-tag").val(data.width); $("#width-tag").val(data.width);
$("#printerType").val(data.sizePreset); $("#printerType").val(data.sizePreset);
$("#qrCheck").prop('checked', data.qr);
$("#dhidCheck").prop('checked', data.dhid);
$("#serialNumberCheck").prop('checked', data.serial_number);
$("#manufacturerCheck").prop('checked', data.manufacturer);
$("#modelCheck").prop('checked', data.model);
}; };
} }
function reset_size() { function reset_settings() {
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
$("#printerType").val('brotherSmall'); $("#printerType").val('brotherSmall');
$("#qrCheck").prop('checked', true);
$("#dhidCheck").prop('checked', true);
$("#serialNumberCheck").prop('checked', false);
$("#manufacturerCheck").prop('checked', false);
$("#modelCheck").prop('checked', false);
change_size(); change_size();
change_check();
} }
function change_size() { function change_size() {
@ -50,11 +68,40 @@ function change_size() {
} }
} }
function change_check() {
if ($("#dhidCheck").prop('checked')) {
$(".dhid").show();
} else {
$(".dhid").hide();
}
if ($("#serialNumberCheck").prop('checked')) {
$(".serial_number").show();
} else {
$(".serial_number").hide();
}
if ($("#manufacturerCheck").prop('checked')) {
$(".manufacturer").show();
} else {
$(".manufacturer").hide();
}
if ($("#modelCheck").prop('checked')) {
$(".model").show();
} else {
$(".model").hide();
}
if ($("#qrCheck").prop('checked')) {
$(".qr").show();
} else {
$(".qr").hide();
}
}
function printpdf() { function printpdf() {
var border = 2; var border = 2;
var line = 5;
var height = parseInt($("#height-tag").val()); var height = parseInt($("#height-tag").val());
var width = parseInt($("#width-tag").val()); var width = parseInt($("#width-tag").val());
img_side = Math.min(height, width) - 2*border; var img_side = Math.min(height, width) - 2*border;
max_tag_side = (Math.max(height, width)/2) + border; max_tag_side = (Math.max(height, width)/2) + border;
if (max_tag_side < img_side) { if (max_tag_side < img_side) {
max_tag_side = img_side + 2*border; max_tag_side = img_side + 2*border;
@ -62,17 +109,60 @@ function printpdf() {
min_tag_side = (Math.min(height, width)/2) + border; min_tag_side = (Math.min(height, width)/2) + border;
var last_tag_code = ''; var last_tag_code = '';
if ($("#serialNumberCheck").prop('checked')) {
height += line;
};
if ($("#manufacturerCheck").prop('checked')) {
height += line;
};
if ($("#modelCheck").prop('checked')) {
height += line;
};
var pdf = new jsPDF('l', 'mm', [width, height]); var pdf = new jsPDF('l', 'mm', [width, height]);
$(".tag").map(function(x, y) { $(".tag").map(function(x, y) {
if (x != 0){ if (x != 0){
pdf.addPage(); pdf.addPage();
console.log(x)
}; };
var space = line + border;
if ($("#qrCheck").prop('checked')) {
space += img_side;
}
var tag = $(y).text(); var tag = $(y).text();
last_tag_code = tag; last_tag_code = tag;
if ($("#qrCheck").prop('checked')) {
var imgData = $('#'+tag+' img').attr("src"); var imgData = $('#'+tag+' img').attr("src");
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side); pdf.addImage(imgData, 'PNG', border, border, img_side, img_side);
};
if ($("#dhidCheck").prop('checked')) {
if ($("#qrCheck").prop('checked')) {
pdf.setFontSize(15);
pdf.text(tag, max_tag_side, min_tag_side); pdf.text(tag, max_tag_side, min_tag_side);
} else {
pdf.setFontSize(15);
pdf.text(tag, border, space);
space += line;
}
};
if ($("#serialNumberCheck").prop('checked')) {
var sn = $(y).data('serial-number');
pdf.setFontSize(12);
pdf.text(sn, border, space);
space += line;
};
if ($("#manufacturerCheck").prop('checked')) {
var sn = $(y).data('manufacturer');
pdf.setFontSize(12);
pdf.text(sn, border, space);
space += line;
};
if ($("#modelCheck").prop('checked')) {
var sn = $(y).data('model');
pdf.setFontSize(8);
pdf.text(sn, border, space);
space += line;
};
}); });
pdf.save('Tag_'+last_tag_code+'.pdf'); pdf.save('Tag_'+last_tag_code+'.pdf');

View File

@ -29,6 +29,7 @@
<!-- Template Main CSS File --> <!-- Template Main CSS File -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/devicehub.css') }}" rel="stylesheet">
<!-- ======================================================= <!-- =======================================================
* Template Name: NiceAdmin - v2.2.0 * Template Name: NiceAdmin - v2.2.0

View File

@ -191,15 +191,6 @@
</ul> </ul>
</li><!-- End Temporal Lots Nav --> </li><!-- End Temporal Lots Nav -->
<li class="nav-heading">Utils</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('labels.label_list')}}">
<i class="bi bi-tags"></i>
<span>Tags</span>
</a>
</li><!-- End Tags Page Nav -->
</ul> </ul>
</aside><!-- End Sidebar--> </aside><!-- End Sidebar-->

View File

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

View File

@ -3,14 +3,14 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Adding to a tag</h5> <h5 class="modal-title">Adding to a unique identifier</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form action="{{ url_for('inventory.tag_devices_add') }}" method="post"> <form action="{{ url_for('inventory.tag_devices_add') }}" method="post">
{{ form_tag_device.csrf_token }} {{ form_tag_device.csrf_token }}
<div class="modal-body"> <div class="modal-body">
Please write a name of a tag Please write a name of a unique identifier
<select class="form-control selectpicker" id="selectTag" name="tag" data-live-search="true"> <select class="form-control selectpicker" id="selectTag" name="tag" data-live-search="true">
{% for tag in tags %} {% for tag in tags %}
<option value="{{ tag.id }}">{{ tag.id }}</option> <option value="{{ tag.id }}">{{ tag.id }}</option>
@ -18,7 +18,7 @@
</select> </select>
<input class="devicesList" type="hidden" name="device" /> <input class="devicesList" type="hidden" name="device" />
<p class="text-danger pol"> <p class="text-danger pol">
You need select first one device and only one for add this in a tag You need select first one device and only one for add this in a unique identifier
</p> </p>
</div> </div>

View File

@ -32,21 +32,25 @@
<div class="card-body pt-3"> <div class="card-body pt-3">
<!-- Bordered Tabs --> <!-- Bordered Tabs -->
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between row">
<h3><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3> <h3 class="col-sm-12 col-md-5"><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3>
<div><!-- 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>
@ -69,7 +73,7 @@
</ul> </ul>
{% endif %} {% endif %}
<div class="tab-content pt-5"> <div class="tab-content pt-1">
<div id="devices-list" class="tab-pane fade devices-list active show"> <div id="devices-list" class="tab-pane fade devices-list active show">
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label> <label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
<div class="btn-group dropdown ml-1"> <div class="btn-group dropdown ml-1">
@ -79,9 +83,9 @@
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span> <span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnLots" style="width: 300px;" id="dropDownLotsSelector"> <ul class="dropdown-menu" aria-labelledby="btnLots" id="dropDownLotsSelector">
<h6 class="dropdown-header">Select some devices to manage lots</h6> <h6 class="dropdown-header">Select some devices to manage lots</h6>
<ul style="list-style-type: none; margin: 0; padding: 0;" class="mx-3" id="LotsSelector"></ul> <ul class="mx-3" id="LotsSelector"></ul>
<li><hr /></li> <li><hr /></li>
<li> <li>
<a href="#" class="dropdown-item" id="ApplyDeviceLots"> <a href="#" class="dropdown-item" id="ApplyDeviceLots">
@ -91,7 +95,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
New Actions New Actions
@ -178,7 +182,7 @@
</ul> </ul>
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-reply"></i> <i class="bi bi-reply"></i>
Exports Exports
@ -212,26 +216,43 @@
</ul> </ul>
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnUniqueID" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i> <i class="bi bi-tag"></i>
Tags Unique Identifiers
</button> </button>
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span> <span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span> <span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnTags"> <ul class="dropdown-menu" aria-labelledby="btnUniqueID">
<li> <li>
<a href="javascript:addTag()" class="dropdown-item"> <a href="javascript:addTag()" class="dropdown-item">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Add Tag to selected Device Add Unique Identifier to selected Device
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:removeTag()" class="dropdown-item"> <a href="javascript:removeTag()" class="dropdown-item">
<i class="bi bi-x"></i> <i class="bi bi-x"></i>
Remove Tag from selected Device Remove Unique Identifier from selected Device
</a> </a>
</li> </li>
<li>
<a class="dropdown-item" href="{{ url_for('labels.label_list')}}">
<i class="bi bi-tools"></i>
Unique Identifier Management
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i>
Labels
</button>
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnTags">
<li> <li>
<form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}"> <form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}">
{% for f in form_print_labels %} {% for f in form_print_labels %}
@ -246,7 +267,7 @@
</ul> </ul>
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-laptop"></i> <i class="bi bi-laptop"></i>
New Device New Device
@ -314,7 +335,7 @@
<th scope="col">Select</th> <th scope="col">Select</th>
<th scope="col">Title</th> <th scope="col">Title</th>
<th scope="col">DHID</th> <th scope="col">DHID</th>
<th scope="col">Tags</th> <th scope="col">Unique Identifiers</th>
<th scope="col">Status</th> <th scope="col">Status</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Update</th> <th scope="col" data-type="date" data-format="DD-MM-YYYY">Update</th>
</tr> </tr>
@ -332,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>
@ -407,7 +431,9 @@
<!-- Custom Code --> <!-- Custom Code -->
<script> <script>
const table = new simpleDatatables.DataTable("table") const table = new simpleDatatables.DataTable("table", {
perPage: 20
})
</script> </script>
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script> <script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
{% endblock main %} {% endblock main %}

View File

@ -18,8 +18,8 @@
<div class="card-body"> <div class="card-body">
<div class="pt-4 pb-2"> <div class="pt-4 pb-2">
<h1 class="card-title text-center pb-0 fs-4">Unlink Tag from Device</h1> <h1 class="card-title text-center pb-0 fs-4">Unlink Unique Identifier from Device</h1>
<p class="text-center small">Please enter a code for the tag.</p> <p class="text-center small">Please enter a code for the unique identifier.</p>
{% if form.form_errors %} {% if form.form_errors %}
<p class="text-danger"> <p class="text-danger">
{% for error in form.form_errors %} {% for error in form.form_errors %}
@ -33,10 +33,10 @@
{{ form.csrf_token }} {{ form.csrf_token }}
<div> <div>
<label for="tag" class="form-label">Tag</label> <label for="tag" class="form-label">Unique Identifier</label>
<div class="input-group has-validation"> <div class="input-group has-validation">
{{ form.tag(class_="form-control") }} {{ form.tag(class_="form-control") }}
<div class="invalid-feedback">Please select tag.</div> <div class="invalid-feedback">Please select unique identifier.</div>
</div> </div>
{% if form.tag.errors %} {% if form.tag.errors %}
<p class="text-danger"> <p class="text-danger">

View File

@ -5,8 +5,8 @@
<h1>Inventory</h1> <h1>Inventory</h1>
<nav> <nav>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
<li class="breadcrumb-item active">Tag details {{ tag.id }}</li> <li class="breadcrumb-item active">Unique Identifier details {{ tag.id }}</li>
</ol> </ol>
</nav> </nav>
</div><!-- End Page Title --> </div><!-- End Page Title -->
@ -26,7 +26,7 @@
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-4 label ">Type</div> <div class="col-lg-3 col-md-4 label ">Type</div>
<div class="col-lg-9 col-md-8">{% if tag.provider %}UnNamed Tag{% else %}Named{% endif %}</div> <div class="col-lg-9 col-md-8">{% if tag.provider %}UnNamed Unique Identifier{% else %}Named Unique Identifier{% endif %}</div>
</div> </div>
<div class="row"> <div class="row">
@ -43,17 +43,50 @@
<h5 class="card-title">Print Label</h5> <h5 class="card-title">Print Label</h5>
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<div style="width:256px; height:148px; border: solid 1px; padding: 10px;"> <div style="width:256px; min-height:148px; border: solid 1px; padding: 10px;">
<div id="print"> <div id="print">
<div class="row"> <div class="row">
<div class="col"> <div class="col qr">
<div id="{{ tag.id }}"></div> <div id="{{ tag.id }}"></div>
</div> </div>
<div class="col dhid">
<div style="padding-top: 55px">
{% if tag.device %}
<b class="tag" data-serial-number="{{ tag.device.serial_number or '' }}"
data-manufacturer="{{ tag.device.manufacturer or '' }}"
data-model="{{ tag.device.model or '' }}">{{ tag.id }}</b>
{% else %}
<b class="tag" data-serial-number=""
data-manufacturer=""
data-model="">{{ tag.id }}</b>
{% endif %}
</div>
</div>
</div>
{% if tag.device %}
<div class="row serial_number" style="display: none">
<div class="col"> <div class="col">
<div style="padding-top: 55px"><b class="tag">{{ tag.id }}</b></div> <div>
<b>{{ tag.device.serial_number or '' }}</b>
</div> </div>
</div> </div>
</div> </div>
<div class="row manufacturer" style="display: none">
<div class="col">
<div>
<b>{{ tag.device.manufacturer or '' }}</b>
</div>
</div>
</div>
<div class="row model" style="display: none">
<div class="col">
<div>
<span style="font-size: 12px;">{{ tag.device.model or '' }}</span>
</div>
</div>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
<div class="col-lg-5 col-md-6 label"> <div class="col-lg-5 col-md-6 label">
@ -84,20 +117,43 @@
<span class="input-group-text">mm</span> <span class="input-group-text">mm</span>
</div> </div>
</div> </div>
{% if tag.device %}
<div class="col-sm-10">
<div class="form-switch">
<input class="form-check-input" name="qr" type="checkbox" id="qrCheck" checked="">
<label class="form-check-label" for="qrCheck">QR</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="dhid" type="checkbox" id="dhidCheck" checked="">
<label class="form-check-label" for="dhidCheck">Unique Identifier</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="serial_number" type="checkbox" id="serialNumberCheck">
<label class="form-check-label" for="serialNumberCheck">Serial number</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="manufacturer" type="checkbox" id="manufacturerCheck">
<label class="form-check-label" for="manufacturerCheck">Manufacturer</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="model" type="checkbox" id="modelCheck">
<label class="form-check-label" for="modelCheck">Model</label>
</div> </div>
</div> </div>
<div class="row"> {% endif %}
<div class="row mt-5">
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<a href="javascript:printpdf()" class="btn btn-success">Print</a> <a href="javascript:printpdf()" class="btn btn-success">Print labels</a>
</div> </div>
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<a href="javascript:save_size()" class="btn btn-primary">Save</a> <a href="javascript:save_settings()" class="btn btn-primary">Save settings</a>
</div> </div>
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<a href="javascript:reset_size()" class="btn btn-danger">Reset</a> <a href="javascript:reset_settings()" class="btn btn-danger">Reset settings</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,18 +19,18 @@
<div class="card-body pt-3"> <div class="card-body pt-3">
<!-- Bordered Tabs --> <!-- Bordered Tabs -->
<div class="btn-group dropdown ml-1"> <div class="btn-group dropdown m-1">
<a href="{{ url_for('labels.tag_add')}}" type="button" class="btn btn-primary"> <a href="{{ url_for('labels.tag_add')}}" type="button" class="btn btn-primary">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Create Named Tag Create Named Unique Identifier
<span class="caret"></span> <span class="caret"></span>
</a> </a>
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown m-1" uib-dropdown="">
<a href="{{ url_for('labels.tag_unnamed_add')}}" type="button" class="btn btn-primary"> <a href="{{ url_for('labels.tag_unnamed_add')}}" type="button" class="btn btn-primary">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Create UnNamed Tag Create UnNamed Unique Identifier
<span class="caret"></span> <span class="caret"></span>
</a> </a>
</div> </div>
@ -53,11 +53,11 @@
{% for tag in tags %} {% for tag in tags %}
<tr> <tr>
<td><a href="{{ url_for('labels.label_details', id=tag.id) }}">{{ tag.id }}</a></td> <td><a href="{{ url_for('labels.label_details', id=tag.id) }}">{{ tag.id }}</a></td>
<td>{% if tag.provider %}Unnamed tag {% else %}Named tag{% endif %}</td> <td>{% if tag.provider %}Unnamed unique Identifier {% else %}Named unique identifier{% endif %}</td>
<td>{{ tag.get_provider }}</td> <td>{{ tag.get_provider }}</td>
<td> <td>
{% if tag.device %} {% if tag.device %}
<a href={{ url_for('inventory.device_details', id=tag.device.devicehub_id)}}> <a href="{{ url_for('inventory.device_details', id=tag.device.devicehub_id)}}">
{{ tag.device.verbose_name }} {{ tag.device.verbose_name }}
</a> </a>
{% endif %} {% endif %}

View File

@ -5,7 +5,7 @@
<h1>Print Labels</h1> <h1>Print Labels</h1>
<nav> <nav>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
<li class="breadcrumb-item active">Print Labels</li> <li class="breadcrumb-item active">Print Labels</li>
</ol> </ol>
</nav> </nav>
@ -24,16 +24,39 @@
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
{% for tag in tags %} {% for dev in devices %}
<div style="width:256px; height:148px; border: solid 1px; padding: 10px;"> <div style="width:256px; min-height:148px; border: solid 1px; padding: 10px;">
<div id="print"> <div>
<div class="row"> <div class="row">
<div class="col"> <div class="col qr">
<div id="{{ tag.id }}"></div> <div id="{{ dev.devicehub_id }}"></div>
</div> </div>
<div class="col"> <div class="col dhid">
<div style="padding-top: 55px"> <div style="padding-top: 55px">
<b class="tag">{{ tag.id }}</b> <b class="tag" data-serial-number="{{ dev.serial_number or '' }}"
data-manufacturer="{{ dev.manufacturer or '' }}"
data-model="{{ dev.model or '' }}">{{ dev.devicehub_id }}</b>
</div>
</div>
</div>
<div class="row serial_number" style="display: none">
<div class="col">
<div>
<b>{{ dev.serial_number or '' }}</b>
</div>
</div>
</div>
<div class="row manufacturer" style="display: none">
<div class="col">
<div>
<b>{{ dev.manufacturer or '' }}</b>
</div>
</div>
</div>
<div class="row model" style="display: none">
<div class="col">
<div>
<span style="font-size: 12px;">{{ dev.model or '' }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -71,20 +94,41 @@
<span class="input-group-text">mm</span> <span class="input-group-text">mm</span>
</div> </div>
</div> </div>
<div class="col-sm-10">
<div class="form-switch">
<input class="form-check-input" name="qr" type="checkbox" id="qrCheck" checked="">
<label class="form-check-label" for="qrCheck">QR</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="dhid" type="checkbox" id="dhidCheck" checked="">
<label class="form-check-label" for="dhidCheck">Dhid</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="serial_number" type="checkbox" id="serialNumberCheck">
<label class="form-check-label" for="serialNumberCheck">Serial number</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="manufacturer" type="checkbox" id="manufacturerCheck">
<label class="form-check-label" for="manufacturerCheck">Manufacturer</label>
</div>
<div class="form-switch">
<input class="form-check-input" name="model" type="checkbox" id="modelCheck">
<label class="form-check-label" for="modelCheck">Model</label>
</div> </div>
</div> </div>
<div class="row mt-5"> <div class="row mt-5">
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<a href="javascript:printpdf()" class="btn btn-success">Print</a> <a href="javascript:printpdf()" class="btn btn-success">Print labels</a>
</div> </div>
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<a href="javascript:save_size()" class="btn btn-primary">Save</a> <a href="javascript:save_settings()" class="btn btn-primary">Save settings</a>
</div> </div>
<div class="col-lg-3 col-md-4"> <div class="col-lg-3 col-md-4">
<a href="javascript:reset_size()" class="btn btn-danger">Reset</a> <a href="javascript:reset_settings()" class="btn btn-danger">Reset settings</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -96,8 +140,8 @@
<script src="{{ url_for('static', filename='js/jspdf.min.js') }}"></script> <script src="{{ url_for('static', filename='js/jspdf.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script> <script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
{% for tag in tags %} {% for dev in devices %}
qr_draw("{{ url_for('inventory.device_details', id=tag.device.devicehub_id, _external=True) }}", "#{{ tag.id }}") qr_draw("{{ url_for('inventory.device_details', id=dev.devicehub_id, _external=True) }}", "#{{ dev.devicehub_id }}")
{% endfor %} {% endfor %}
</script> </script>
{% endblock main %} {% endblock main %}

View File

@ -5,7 +5,7 @@
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<nav> <nav>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
<li class="breadcrumb-item">{{ page_title }}</li> <li class="breadcrumb-item">{{ page_title }}</li>
</ol> </ol>
</nav> </nav>
@ -19,8 +19,8 @@
<div class="card-body"> <div class="card-body">
<div class="pt-4 pb-2"> <div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Add a new Tag</h5> <h5 class="card-title text-center pb-0 fs-4">Add a new Unique Identifier</h5>
<p class="text-center small">Please enter a code for the tag.</p> <p class="text-center small">Please enter a code for the unique identifier.</p>
{% if form.form_errors %} {% if form.form_errors %}
<p class="text-danger"> <p class="text-danger">
{% for error in form.form_errors %} {% for error in form.form_errors %}
@ -37,7 +37,7 @@
<label for="code" class="form-label">code</label> <label for="code" class="form-label">code</label>
<div class="input-group has-validation"> <div class="input-group has-validation">
<input type="text" name="code" class="form-control" required value="{{ form.code.data|default('', true) }}"> <input type="text" name="code" class="form-control" required value="{{ form.code.data|default('', true) }}">
<div class="invalid-feedback">Please enter a code of the tag.</div> <div class="invalid-feedback">Please enter a code of the unique identifier.</div>
</div> </div>
{% if form.code.errors %} {% if form.code.errors %}
<p class="text-danger"> <p class="text-danger">

View File

@ -5,7 +5,7 @@
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<nav> <nav>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
<li class="breadcrumb-item">{{ page_title }}</li> <li class="breadcrumb-item">{{ page_title }}</li>
</ol> </ol>
</nav> </nav>
@ -19,8 +19,8 @@
<div class="card-body"> <div class="card-body">
<div class="pt-4 pb-2"> <div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Add new Unnamed Tags</h5> <h5 class="card-title text-center pb-0 fs-4">Add new Unnamed Unique Identifiers</h5>
<p class="text-center small">Please enter a number of the tags to issue.</p> <p class="text-center small">Please enter a number of the unique identifiers to issue.</p>
{% if form.form_errors %} {% if form.form_errors %}
<p class="text-danger"> <p class="text-danger">
{% for error in form.form_errors %} {% for error in form.form_errors %}
@ -37,7 +37,7 @@
<label for="code" class="form-label">Amount</label> <label for="code" class="form-label">Amount</label>
<div class="input-group has-validation"> <div class="input-group has-validation">
{{ form.amount(class_="form-control") }} {{ form.amount(class_="form-control") }}
<div class="invalid-feedback">Please enter a number of the tags to issue.</div> <div class="invalid-feedback">Please enter a number of the unique identifiers to issue.</div>
</div> </div>
{% if form.amount.errors %} {% if form.amount.errors %}
<p class="text-danger"> <p class="text-danger">

View File

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

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "workspace",
"version": "1.0.0",
"description": "Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org)",
"main": "index.js",
"directories": {
"doc": "docs",
"example": "examples",
"test": "tests"
},
"devDependencies": {
"eslint": "^8.13.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0"
},
"scripts": {
"lint:report": "eslint ereuse_devicehub --ext .js --output-file eslint_report.json --format json",
"lint:fix": "eslint ereuse_devicehub --ext .js --fix"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0"
}
}