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

This commit is contained in:
Cayo Puigdefabregas 2022-04-19 11:03:05 +02:00
commit efbbdf3d44
30 changed files with 1275 additions and 682 deletions

View File

@ -8,6 +8,10 @@ ml).
## master ## master
## testing ## testing
- [added] #219 Add functionality to searchbar (Lots and devices).
- [changed] #211 Print DHID-QR label for selected devices.
- [changed] #218 Add reactivity to device lots.
- [fixed] #214 Login workflow
## [2.0.0] - 2022-03-15 ## [2.0.0] - 2022-03-15
First server render HTML version. Completely rewrites views of angular JS client on flask. First server render HTML version. Completely rewrites views of angular JS client on flask.

134
README.md Normal file
View File

@ -0,0 +1,134 @@
#Devicehub
Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org)
This README explains how to install and use Devicehub. [The documentation](http://devicehub.ereuse.org) explains the concepts and the API.
Devicehub is built with [Teal](https://github.com/ereuse/teal) and [Flask](http://flask.pocoo.org).
# Installing
The requirements are:
- Python 3.7.3 or higher. In debian 10 is `# apt install python3`.
- [PostgreSQL 11 or higher](https://www.postgresql.org/download/).
- Weasyprint [dependencie](http://weasyprint.readthedocs.io/en/stable/install.html)
Install Devicehub with *pip*: `pip3 install -U -r requirements.txt -e .`
# Running
Create a PostgreSQL database called *devicehub* by running [create-db](examples/create-db.sh):
- In Linux, execute the following two commands (adapt them to your distro):
1. `sudo su - postgres`.
2. `bash examples/create-db.sh devicehub dhub`, and password `ereuse`.
- In MacOS: `bash examples/create-db.sh devicehub dhub`, and password `ereuse`.
Configure project using environment file (you can use provided example as quickstart):
```bash
$ cp examples/env.example .env
```
Using the `dh` tool for set up with one or multiple inventories.
Create the tables in the database by executing:
```bash
$ export dhi=dbtest; dh inv add --common --name dbtest
```
Finally, run the app:
```bash
$ export dhi=dbtest;dh run --debugger
```
The error bdist_wheel can happen when you work with a *virtual environment*.
To fix it, install in the *virtual environment* wheel
package. `pip3 install wheel`
## Multiple instances
Devicehub can run as a single inventory or with multiple inventories, each inventory being an instance of the `devicehub`. To add a new inventory execute:
```bash
$ export dhi=dbtest; dh inv add --name dbtest
```
Note: The `dh` command is like `flask`, but it allows you to create and delete instances, and interface to them directly.
# Testing
1. `git clone` this project.
2. Create a database for testing executing `create-db.sh` like the normal installation but changing the first parameter from `devicehub` to `dh_test`: `create-db.sh dh_test dhub` and password `ereuse`.
3. Execute at the root folder of the project `python3 setup.py test`.
# Migrations
At this stage, migration files are created manually.
Set up the database:
```bash
$ sudo su - postgres
$ bash $PATH_TO_DEVIHUBTEAL/examples/create-db.sh devicehub dhub
```
Initialize the database:
```bash
$ export dhi=dbtest; dh inv add --common --name dbtest
```
This command will create the schemas, tables in the specified database.
Then we need to stamp the initial migration.
```bash
$ alembic stamp head
```
This command will set the revision **fbb7e2a0cde0_initial** as our initial migration.
For more info in migration stamping please see https://alembic.sqlalchemy.org/en/latest/cookbook.html
Whenever a change needed eg to create a new schema, alter an existing table, column or perform any
operation on tables, create a new revision file:
```bash
$ alembic revision -m "A table change"
```
This command will create a new revision file with name `<revision_id>_a_table_change`.
Edit the generated file with the necessary operations to perform the migration:
```bash
$ alembic edit <revision_id>
```
Apply migrations using:
```bash
$ alembic -x inventory=dbtest upgrade head
```
Then to go back to previous db version:
```bash
$ alembic -x inventory=dbtest downgrade <revision_id>
```
To see a full list of migrations use
```bash
$ alembic history
```
## Generating the docs
1. `git clone` this project.
2. Install plantuml. In Debian 9 is `# apt install plantuml`.
3. Execute `pip3 install -e .[docs]` in the project root folder.
4. Go to `<project root folder>/docs` and execute `make html`. Repeat this step to generate new docs.
To auto-generate the docs do `pip3 install -e .[docs-auto]`, then execute, in the root folder of the project `sphinx-autobuild docs docs/_build/html`.

View File

@ -1,159 +0,0 @@
Devicehub
#########
Devicehub is a distributed IT Asset Management System focused in reusing
devices, created under the project
`eReuse.org <https://www.ereuse.org>`__.
This README explains how to install and use Devicehub.
`The documentation <http://devicehub.ereuse.org>`_ explains the concepts
and the API.
Devicehub is built with `Teal <https://github.com/ereuse/teal>`__ and
`Flask <http://flask.pocoo.org>`__.
Installing
**********
The requirements are:
- Python 3.7.3 or higher. In debian 10 is ``# apt install python3``.
- `PostgreSQL 11 or higher <https://www.postgresql.org/download/>`__.
- Weasyprint
`dependencies <http://weasyprint.readthedocs.io/en/stable/install.html>`__.
Install Devicehub with *pip*:
``pip3 install -U -r requirements.txt -e .``.
Running
*******
Create a PostgreSQL database called *devicehub* by running
`create-db <examples/create-db.sh>`__:
- In Linux, execute the following two commands (adapt them to your distro):
1. ``sudo su - postgres``.
2. ``bash examples/create-db.sh devicehub dhub``, and password
``ereuse``.
- In MacOS: ``bash examples/create-db.sh devicehub dhub``, and password
``ereuse``.
Configure project using environment file (you can use provided example as quickstart):
.. code:: bash
$ cp examples/env.example .env
Using the `dh` tool for set up with one or multiple inventories.
Create the tables in the database by executing:
.. code:: bash
$ export dhi=dbtest; dh inv add --common --name dbtest
Finally, run the app:
.. code:: bash
$ export dhi=dbtest;dh run --debugger
The error bdist_wheel can happen when you work with a *virtual environment*.
To fix it, install in the *virtual environment* wheel
package. ``pip3 install wheel``
Multiple instances
------------------
Devicehub can run as a single inventory or with multiple inventories,
each inventory being an instance of the ``devicehub``. To add a new inventory
execute:
.. code:: bash
$ export dhi=dbtest; dh inv add --name dbtest
Note: The ``dh`` command is like ``flask``, but
it allows you to create and delete instances, and interface to them
directly.
Testing
*******
1. ``git clone`` this project.
2. Create a database for testing executing ``create-db.sh`` like the
normal installation but changing the first parameter from
``devicehub`` to ``dh_test``: ``create-db.sh dh_test dhub`` and
password ``ereuse``.
3. Execute at the root folder of the project ``python3 setup.py test``.
Migrations
**********
At this stage, migration files are created manually.
Set up the database:
.. code:: bash
$ sudo su - postgres
$ bash $PATH_TO_DEVIHUBTEAL/examples/create-db.sh devicehub dhub
Initialize the database:
.. code:: bash
$ export dhi=dbtest; dh inv add --common --name dbtest
This command will create the schemas, tables in the specified database.
Then we need to stamp the initial migration.
.. code:: bash
$ alembic stamp head
This command will set the revision **fbb7e2a0cde0_initial** as our initial migration.
For more info in migration stamping please see https://alembic.sqlalchemy.org/en/latest/cookbook.html
Whenever a change needed eg to create a new schema, alter an existing table, column or perform any
operation on tables, create a new revision file:
.. code:: bash
$ alembic revision -m "A table change"
This command will create a new revision file with name `<revision_id>_a_table_change`.
Edit the generated file with the necessary operations to perform the migration:
.. code:: bash
$ alembic edit <revision_id>
Apply migrations using:
.. code:: bash
$ alembic -x inventory=dbtest upgrade head
Then to go back to previous db version:
.. code:: bash
$ alembic -x inventory=dbtest downgrade <revision_id>
To see a full list of migrations use
.. code:: bash
$ alembic history
Generating the docs
*******************
1. ``git clone`` this project.
2. Install plantuml. In Debian 9 is ``# apt install plantuml``.
3. Execute ``pip3 install -e .[docs]`` in the project root folder.
4. Go to ``<project root folder>/docs`` and execute ``make html``.
Repeat this step to generate new docs.
To auto-generate the docs do ``pip3 install -e .[docs-auto]``, then
execute, in the root folder of the project
``sphinx-autobuild docs docs/_build/html``.

49
development-setup.md Executable file
View File

@ -0,0 +1,49 @@
# Setup developement project
## Installing
complete this steps from [README - Installing](README.md#installing)
## Setup project
Create a PostgreSQL database called devicehub by running [create-db](examples/create-db.sh):
- Start postgresDB
- `bash examples/create-db.sh devicehub dhub, and password ereuse.`
- `cp examples/env.example .env`
Create a secretkey and add into `.env`
```bash
echo "SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex())')" >> .env
```
Using the dh tool for set up with one or multiple inventories. Create the tables in the database by executing:
```bash
export dhi=dbtest; dh inv add --common --name dbtest
```
Create a demo table
```bash
export dhi=dbtest; dh dummy
```
## Run project
Run the app
```bash
export FLASK_APP=app.py; export FLASK_ENV=development; flask run --debugger
```
Finally login into `localhost:5000/login/`
- User: user@dhub.com
- Pass: 1234
## Troubleshooting
- If when execute dh command it thows an error, install this dependencies in your distro
- `sudo apt install -y libpango1.0-0 libcairo2 libpq-dev`

View File

@ -10,7 +10,7 @@ from ereuse_utils.session import DevicehubClient
from flask import _app_ctx_stack, g from flask import _app_ctx_stack, g
from flask_login import LoginManager, current_user from flask_login import LoginManager, current_user
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from teal.db import SchemaSQLAlchemy from teal.db import ResourceNotFound, SchemaSQLAlchemy
from teal.teal import Teal from teal.teal import Teal
from ereuse_devicehub.auth import Auth from ereuse_devicehub.auth import Auth
@ -29,32 +29,49 @@ class Devicehub(Teal):
Dummy = Dummy Dummy = Dummy
jinja_environment = Environment jinja_environment = Environment
def __init__(self, def __init__(
inventory: str, self,
config: DevicehubConfig = DevicehubConfig(), inventory: str,
db: SQLAlchemy = db, config: DevicehubConfig = DevicehubConfig(),
import_name=__name__.split('.')[0], db: SQLAlchemy = db,
static_url_path=None, import_name=__name__.split('.')[0],
static_folder='static', static_url_path=None,
static_host=None, static_folder='static',
host_matching=False, static_host=None,
subdomain_matching=False, host_matching=False,
template_folder='templates', subdomain_matching=False,
instance_path=None, template_folder='templates',
instance_relative_config=False, instance_path=None,
root_path=None, instance_relative_config=False,
Auth: Type[Auth] = Auth): root_path=None,
Auth: Type[Auth] = Auth,
):
assert inventory assert inventory
super().__init__(config, db, inventory, import_name, static_url_path, static_folder, super().__init__(
static_host, config,
host_matching, subdomain_matching, template_folder, instance_path, db,
instance_relative_config, root_path, False, Auth) inventory,
import_name,
static_url_path,
static_folder,
static_host,
host_matching,
subdomain_matching,
template_folder,
instance_path,
instance_relative_config,
root_path,
False,
Auth,
)
self.id = inventory self.id = inventory
"""The Inventory ID of this instance. In Teal is the app.schema.""" """The Inventory ID of this instance. In Teal is the app.schema."""
self.dummy = Dummy(self) self.dummy = Dummy(self)
@self.cli.group(short_help='Inventory management.', @self.cli.group(
help='Manages the inventory {}.'.format(os.environ.get('dhi'))) short_help='Inventory management.',
help='Manages the inventory {}.'.format(os.environ.get('dhi')),
)
def inv(): def inv():
pass pass
@ -69,43 +86,68 @@ class Devicehub(Teal):
# configure Flask-Login # configure Flask-Login
login_manager = LoginManager() login_manager = LoginManager()
login_manager.init_app(self) login_manager.init_app(self)
login_manager.login_view = "core.login"
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return User.query.get(user_id) # TODO(@slamora) refactor when teal library has been drop.
# `load_user` expects None if the user ID is invalid or the
# session has expired so we need to handle Exception raised
# by teal (it's overriding default behaviour of flask-sqlalchemy
# which already returns None)
try:
return User.query.get(user_id)
except ResourceNotFound:
return None
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
@click.option('--name', '-n', @click.option(
default='Test 1', '--name', '-n', default='Test 1', help='The human name of the inventory.'
help='The human name of the inventory.') )
@click.option('--org-name', '-on', @click.option(
default='My Organization', '--org-name',
help='The name of the default organization that owns this inventory.') '-on',
@click.option('--org-id', '-oi', default='My Organization',
default='foo-bar', help='The name of the default organization that owns this inventory.',
help='The Tax ID of the organization.') )
@click.option('--tag-url', '-tu', @click.option(
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False), '--org-id', '-oi', default='foo-bar', help='The Tax ID of the organization.'
default='http://example.com', )
help='The base url (scheme and host) of the tag provider.') @click.option(
@click.option('--tag-token', '-tt', '--tag-url',
type=click.UUID, '-tu',
default='899c794e-1737-4cea-9232-fdc507ab7106', type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
help='The token provided by the tag provider. It is an UUID.') default='http://example.com',
@click.option('--erase/--no-erase', help='The base url (scheme and host) of the tag provider.',
default=False, )
help='Delete the schema before? ' @click.option(
'If --common is set this includes the common database.') '--tag-token',
@click.option('--common/--no-common', '-tt',
default=False, type=click.UUID,
help='Creates common databases. Only execute if the database is empty.') default='899c794e-1737-4cea-9232-fdc507ab7106',
def init_db(self, name: str, help='The token provided by the tag provider. It is an UUID.',
org_name: str, )
org_id: str, @click.option(
tag_url: boltons.urlutils.URL, '--erase/--no-erase',
tag_token: uuid.UUID, default=False,
erase: bool, help='Delete the schema before? '
common: bool): 'If --common is set this includes the common database.',
)
@click.option(
'--common/--no-common',
default=False,
help='Creates common databases. Only execute if the database is empty.',
)
def init_db(
self,
name: str,
org_name: str,
org_id: str,
tag_url: boltons.urlutils.URL,
tag_token: uuid.UUID,
erase: bool,
common: bool,
):
"""Creates an inventory. """Creates an inventory.
This creates the database and adds the inventory to the This creates the database and adds the inventory to the
@ -120,10 +162,14 @@ class Devicehub(Teal):
with click_spinner.spinner(): with click_spinner.spinner():
if erase: if erase:
self.db.drop_all(common_schema=common) self.db.drop_all(common_schema=common)
assert not db.has_schema(self.id), 'Schema {} already exists.'.format(self.id) assert not db.has_schema(self.id), 'Schema {} already exists.'.format(
self.id
)
exclude_schema = 'common' if not common else None exclude_schema = 'common' if not common else None
self._init_db(exclude_schema=exclude_schema) self._init_db(exclude_schema=exclude_schema)
InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token) InventoryDef.set_inventory_config(
name, org_name, org_id, tag_url, tag_token
)
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session) DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
self._init_resources(exclude_schema=exclude_schema) self._init_resources(exclude_schema=exclude_schema)
self.db.session.commit() self.db.session.commit()
@ -138,8 +184,11 @@ class Devicehub(Teal):
return True return True
@click.confirmation_option(prompt='Are you sure you want to delete the inventory {}?' @click.confirmation_option(
.format(os.environ.get('dhi'))) prompt='Are you sure you want to delete the inventory {}?'.format(
os.environ.get('dhi')
)
)
def delete_inventory(self): def delete_inventory(self):
"""Erases an inventory. """Erases an inventory.
@ -161,8 +210,9 @@ class Devicehub(Teal):
def _prepare_request(self): def _prepare_request(self):
"""Prepares request stuff.""" """Prepares request stuff."""
inv = g.inventory = Inventory.current # type: Inventory inv = g.inventory = Inventory.current # type: Inventory
g.tag_provider = DevicehubClient(base_url=inv.tag_provider, g.tag_provider = DevicehubClient(
token=DevicehubClient.encode_token(inv.tag_token)) base_url=inv.tag_provider, token=DevicehubClient.encode_token(inv.tag_token)
)
# NOTE: models init methods expects that current user is # NOTE: models init methods expects that current user is
# available on g.user (e.g. to initialize object owner) # available on g.user (e.g. to initialize object owner)
g.user = current_user g.user = current_user

View File

@ -97,62 +97,6 @@ class FilterForm(FlaskForm):
return ['Desktop', 'Laptop', 'Server'] return ['Desktop', 'Laptop', 'Server']
class LotDeviceForm(FlaskForm):
lot = StringField('Lot', [validators.UUID()])
devices = StringField('Devices', [validators.length(min=1)])
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
self._lot = (
Lot.query.outerjoin(Trade)
.filter(Lot.id == self.lot.data)
.filter(
or_(
Trade.user_from == g.user,
Trade.user_to == g.user,
Lot.owner_id == g.user.id,
)
)
.one()
)
devices = set(self.devices.data.split(","))
self._devices = (
Device.query.filter(Device.id.in_(devices))
.filter(Device.owner_id == g.user.id)
.distinct()
.all()
)
return bool(self._devices)
def save(self, commit=True):
trade = self._lot.trade
if trade:
for dev in self._devices:
if trade not in dev.actions:
trade.devices.add(dev)
if self._devices:
self._lot.devices.update(self._devices)
db.session.add(self._lot)
if commit:
db.session.commit()
def remove(self, commit=True):
if self._devices:
self._lot.devices.difference_update(self._devices)
db.session.add(self._lot)
if commit:
db.session.commit()
class LotForm(FlaskForm): class LotForm(FlaskForm):
name = StringField('Name', [validators.length(min=1)]) name = StringField('Name', [validators.length(min=1)])
@ -485,46 +429,6 @@ class NewDeviceForm(FlaskForm):
return snapshot return snapshot
class TagForm(FlaskForm):
code = StringField('Code', [validators.length(min=1)])
def validate(self, extra_validators=None):
error = ["This value is being used"]
is_valid = super().validate(extra_validators)
if not is_valid:
return False
tag = Tag.query.filter(Tag.id == self.code.data).all()
if tag:
self.code.errors = error
return False
return True
def save(self):
self.instance = Tag(id=self.code.data)
db.session.add(self.instance)
db.session.commit()
return self.instance
def remove(self):
if not self.instance.device and not self.instance.provider:
self.instance.delete()
db.session.commit()
return self.instance
class TagUnnamedForm(FlaskForm):
amount = IntegerField('amount')
def save(self):
num = self.amount.data
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
db.session.add_all(tags)
db.session.commit()
return tags
class TagDeviceForm(FlaskForm): class TagDeviceForm(FlaskForm):
tag = SelectField('Tag', choices=[]) tag = SelectField('Tag', choices=[])
device = StringField('Device', [validators.Optional()]) device = StringField('Device', [validators.Optional()])

View File

@ -7,7 +7,6 @@ import flask_weasyprint
from flask import Blueprint, g, make_response, request, url_for from flask import Blueprint, g, make_response, request, url_for
from flask.views import View from flask.views import View
from flask_login import current_user, login_required from flask_login import current_user, login_required
from requests.exceptions import ConnectionError
from sqlalchemy import or_ from sqlalchemy import or_
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
@ -17,17 +16,15 @@ from ereuse_devicehub.inventory.forms import (
AllocateForm, AllocateForm,
DataWipeForm, DataWipeForm,
FilterForm, FilterForm,
LotDeviceForm,
LotForm, LotForm,
NewActionForm, NewActionForm,
NewDeviceForm, NewDeviceForm,
TagDeviceForm, TagDeviceForm,
TagForm,
TagUnnamedForm,
TradeDocumentForm, TradeDocumentForm,
TradeForm, TradeForm,
UploadSnapshotForm, UploadSnapshotForm,
) )
from ereuse_devicehub.labels.forms import PrintLabelsForm
from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
@ -111,13 +108,13 @@ class DeviceListMix(GenericMixView):
self.context = { self.context = {
'devices': devices, 'devices': devices,
'lots': lots, 'lots': lots,
'form_lot_device': LotDeviceForm(),
'form_tag_device': TagDeviceForm(), 'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action, 'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate, 'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe, 'form_new_datawipe': form_new_datawipe,
'form_new_trade': form_new_trade, 'form_new_trade': form_new_trade,
'form_filter': form_filter, 'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(),
'lot': lot, 'lot': lot,
'tags': tags, 'tags': tags,
'list_devices': list_devices, 'list_devices': list_devices,
@ -154,46 +151,6 @@ class DeviceDetailView(GenericMixView):
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
class LotDeviceAddView(View):
methods = ['POST']
decorators = [login_required]
template_name = 'inventory/device_list.html'
def dispatch_request(self):
form = LotDeviceForm()
if form.validate_on_submit():
form.save(commit=False)
messages.success(
'Add devices to lot "{}" successfully!'.format(form._lot.name)
)
db.session.commit()
else:
messages.error('Error adding devices to lot!')
next_url = request.referrer or url_for('inventory.devicelist')
return flask.redirect(next_url)
class LotDeviceDeleteView(View):
methods = ['POST']
decorators = [login_required]
template_name = 'inventory/device_list.html'
def dispatch_request(self):
form = LotDeviceForm()
if form.validate_on_submit():
form.remove(commit=False)
messages.success(
'Remove devices from lot "{}" successfully!'.format(form._lot.name)
)
db.session.commit()
else:
messages.error('Error removing devices from lot!')
next_url = request.referrer or url_for('inventory.devicelist')
return flask.redirect(next_url)
class LotCreateView(GenericMixView): class LotCreateView(GenericMixView):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
@ -315,91 +272,6 @@ class DeviceCreateView(GenericMixView):
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
class TagListView(View):
methods = ['GET']
decorators = [login_required]
template_name = 'inventory/tag_list.html'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by(Tag.id)
context = {
'lots': lots,
'tags': tags,
'page_title': 'Tags Management',
'version': __version__,
}
return flask.render_template(self.template_name, **context)
class TagAddView(View):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/tag_create.html'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {'page_title': 'New Tag', 'lots': lots, 'version': __version__}
form = TagForm()
if form.validate_on_submit():
form.save()
next_url = url_for('inventory.taglist')
return flask.redirect(next_url)
return flask.render_template(self.template_name, form=form, **context)
class TagAddUnnamedView(View):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/tag_create_unnamed.html'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {
'page_title': 'New Unnamed Tag',
'lots': lots,
'version': __version__,
}
form = TagUnnamedForm()
if form.validate_on_submit():
try:
form.save()
except ConnectionError as e:
logger.error(
"Error while trying to connect to tag server: {}".format(e)
)
msg = (
"Sorry, we cannot create the unnamed tags requested because "
"some error happens while connecting to the tag server!"
)
messages.error(msg)
next_url = url_for('inventory.taglist')
return flask.redirect(next_url)
return flask.render_template(self.template_name, form=form, **context)
class TagDetailView(View):
decorators = [login_required]
template_name = 'inventory/tag_detail.html'
def dispatch_request(self, id):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
tag = (
Tag.query.filter(Tag.owner_id == current_user.id).filter(Tag.id == id).one()
)
context = {
'lots': lots,
'tag': tag,
'page_title': '{} Tag'.format(tag.code),
'version': __version__,
}
return flask.render_template(self.template_name, **context)
class TagLinkDeviceView(View): class TagLinkDeviceView(View):
methods = ['POST'] methods = ['POST']
decorators = [login_required] decorators = [login_required]
@ -693,12 +565,6 @@ devices.add_url_rule(
devices.add_url_rule( devices.add_url_rule(
'/lot/<string:lot_id>/device/', view_func=DeviceListView.as_view('lotdevicelist') '/lot/<string:lot_id>/device/', view_func=DeviceListView.as_view('lotdevicelist')
) )
devices.add_url_rule(
'/lot/devices/add/', view_func=LotDeviceAddView.as_view('lot_devices_add')
)
devices.add_url_rule(
'/lot/devices/del/', view_func=LotDeviceDeleteView.as_view('lot_devices_del')
)
devices.add_url_rule('/lot/add/', view_func=LotCreateView.as_view('lot_add')) devices.add_url_rule('/lot/add/', view_func=LotCreateView.as_view('lot_add'))
devices.add_url_rule( devices.add_url_rule(
'/lot/<string:id>/del/', view_func=LotDeleteView.as_view('lot_del') '/lot/<string:id>/del/', view_func=LotDeleteView.as_view('lot_del')
@ -716,14 +582,6 @@ devices.add_url_rule(
'/lot/<string:lot_id>/device/add/', '/lot/<string:lot_id>/device/add/',
view_func=DeviceCreateView.as_view('lot_device_add'), view_func=DeviceCreateView.as_view('lot_device_add'),
) )
devices.add_url_rule('/tag/', view_func=TagListView.as_view('taglist'))
devices.add_url_rule('/tag/add/', view_func=TagAddView.as_view('tag_add'))
devices.add_url_rule(
'/tag/unnamed/add/', view_func=TagAddUnnamedView.as_view('tag_unnamed_add')
)
devices.add_url_rule(
'/tag/<string:id>/', view_func=TagDetailView.as_view('tag_details')
)
devices.add_url_rule( devices.add_url_rule(
'/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add') '/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add')
) )

View File

View File

@ -0,0 +1,73 @@
from flask import g
from flask_wtf import FlaskForm
from wtforms import IntegerField, StringField, validators
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.tag.model import Tag
class TagForm(FlaskForm):
code = StringField('Code', [validators.length(min=1)])
def validate(self, extra_validators=None):
error = ["This value is being used"]
is_valid = super().validate(extra_validators)
if not is_valid:
return False
tag = Tag.query.filter(Tag.id == self.code.data).all()
if tag:
self.code.errors = error
return False
return True
def save(self):
self.instance = Tag(id=self.code.data)
db.session.add(self.instance)
db.session.commit()
return self.instance
def remove(self):
if not self.instance.device and not self.instance.provider:
self.instance.delete()
db.session.commit()
return self.instance
class TagUnnamedForm(FlaskForm):
amount = IntegerField('amount')
def save(self):
num = self.amount.data
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
db.session.add_all(tags)
db.session.commit()
return tags
class PrintLabelsForm(FlaskForm):
devices = StringField(render_kw={'class': "devicesList d-none"})
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not self.devices.data:
return False
device_ids = self.devices.data.split(",")
self._devices = (
Device.query.filter(Device.id.in_(device_ids))
.filter(Device.owner_id == g.user.id)
.distinct()
.all()
)
# print only tags that are DHID
dhids = [x.devicehub_id for x in self._devices]
self._tags = (
Tag.query.filter(Tag.owner_id == g.user.id).filter(Tag.id.in_(dhids)).all()
)
return is_valid

View File

@ -0,0 +1,142 @@
import logging
import flask
from flask import Blueprint, request, url_for
from flask.views import View
from flask_login import current_user, login_required
from requests.exceptions import ConnectionError
from ereuse_devicehub import __version__, messages
from ereuse_devicehub.labels.forms import PrintLabelsForm, TagForm, TagUnnamedForm
from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag
labels = Blueprint('labels', __name__, url_prefix='/labels')
logger = logging.getLogger(__name__)
class TagListView(View):
methods = ['GET']
decorators = [login_required]
template_name = 'labels/label_list.html'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by(Tag.id)
context = {
'lots': lots,
'tags': tags,
'page_title': 'Tags Management',
'version': __version__,
}
return flask.render_template(self.template_name, **context)
class TagAddView(View):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'labels/tag_create.html'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {'page_title': 'New Tag', 'lots': lots, 'version': __version__}
form = TagForm()
if form.validate_on_submit():
form.save()
next_url = url_for('labels.label_list')
return flask.redirect(next_url)
return flask.render_template(self.template_name, form=form, **context)
class TagAddUnnamedView(View):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'labels/tag_create_unnamed.html'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {
'page_title': 'New Unnamed Tag',
'lots': lots,
'version': __version__,
}
form = TagUnnamedForm()
if form.validate_on_submit():
try:
form.save()
except ConnectionError as e:
logger.error(
"Error while trying to connect to tag server: {}".format(e)
)
msg = (
"Sorry, we cannot create the unnamed tags requested because "
"some error happens while connecting to the tag server!"
)
messages.error(msg)
next_url = url_for('labels.label_list')
return flask.redirect(next_url)
return flask.render_template(self.template_name, form=form, **context)
class PrintLabelsView(View):
"""This View is used to print labels from multiple devices"""
methods = ['POST', 'GET']
decorators = [login_required]
template_name = 'labels/print_labels.html'
title = 'Design and implementation of labels'
def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {
'lots': lots,
'page_title': self.title,
'version': __version__,
'referrer': request.referrer,
}
form = PrintLabelsForm()
if form.validate_on_submit():
context['form'] = form
context['tags'] = form._tags
return flask.render_template(self.template_name, **context)
else:
messages.error('Error you need select one or more devices')
next_url = request.referrer or url_for('inventory.devicelist')
return flask.redirect(next_url)
class LabelDetailView(View):
decorators = [login_required]
template_name = 'labels/label_detail.html'
def dispatch_request(self, id):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
tag = (
Tag.query.filter(Tag.owner_id == current_user.id).filter(Tag.id == id).one()
)
context = {
'lots': lots,
'tag': tag,
'page_title': '{} Tag'.format(tag.code),
'version': __version__,
}
return flask.render_template(self.template_name, **context)
labels.add_url_rule('/', view_func=TagListView.as_view('label_list'))
labels.add_url_rule('/add/', view_func=TagAddView.as_view('tag_add'))
labels.add_url_rule(
'/unnamed/add/', view_func=TagAddUnnamedView.as_view('tag_unnamed_add')
)
labels.add_url_rule(
'/print',
view_func=PrintLabelsView.as_view('print_labels'),
)
labels.add_url_rule('/<string:id>/', view_func=LabelDetailView.as_view('label_details'))

View File

@ -0,0 +1,76 @@
const Api = {
/**
* get lots id
* @returns get lots
*/
async get_lots() {
var request = await this.doRequest(API_URLS.lots, "GET", null);
if (request != undefined) return request.items;
throw request;
},
/**
* Get filtered devices info
* @param {number[]} ids devices ids
* @returns full detailed device list
*/
async get_devices(ids) {
var request = await this.doRequest(API_URLS.devices + '?filter={"id": [' + ids.toString() + ']}', "GET", null);
if (request != undefined) return request.items;
throw request;
},
/**
* Get filtered devices info
* @param {number[]} ids devices ids
* @returns full detailed device list
*/
async search_device(id) {
var request = await this.doRequest(API_URLS.devices + '?filter={"devicehub_id": ["' + id + '"]}', "GET", null)
if (request != undefined) return request.items
throw request
},
/**
* Add devices to lot
* @param {number} lotID lot id
* @param {number[]} listDevices list devices id
*/
async devices_add(lotID, listDevices) {
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&");
return await Api.doRequest(queryURL, "POST", null);
},
/**
* Remove devices from a lot
* @param {number} lotID lot id
* @param {number[]} listDevices list devices id
*/
async devices_remove(lotID, listDevices) {
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&");
return await Api.doRequest(queryURL, "DELETE", null);
},
/**
*
* @param {string} url URL to be requested
* @param {String} type Action type
* @param {String | Object} body body content
* @returns
*/
async doRequest(url, type, body) {
var result;
try {
result = await $.ajax({
url: url,
type: type,
headers: { "Authorization": API_URLS.Auth_Token },
body: body
});
return result;
} catch (error) {
console.error(error);
throw error;
}
}
}

View File

@ -4,7 +4,7 @@
* Author: BootstrapMade.com * Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/ * License: https://bootstrapmade.com/license/
*/ */
(function() { (function () {
"use strict"; "use strict";
/** /**
@ -41,7 +41,7 @@
* 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', function (e) {
select('body').classList.toggle('toggle-sidebar') select('body').classList.toggle('toggle-sidebar')
}) })
} }
@ -50,7 +50,7 @@
* 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', function (e) {
select('.search-bar').classList.toggle('search-bar-show') select('.search-bar').classList.toggle('search-bar-show')
}) })
} }
@ -111,7 +111,7 @@
* Initiate tooltips * Initiate tooltips
*/ */
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function(tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl) return new bootstrap.Tooltip(tooltipTriggerEl)
}) })
@ -141,31 +141,31 @@
}], }],
["bold", "italic", "underline", "strike"], ["bold", "italic", "underline", "strike"],
[{ [{
color: [] color: []
}, },
{ {
background: [] background: []
} }
], ],
[{ [{
script: "super" script: "super"
}, },
{ {
script: "sub" script: "sub"
} }
], ],
[{ [{
list: "ordered" list: "ordered"
}, },
{ {
list: "bullet" list: "bullet"
}, },
{ {
indent: "-1" indent: "-1"
}, },
{ {
indent: "+1" indent: "+1"
} }
], ],
["direction", { ["direction", {
align: [] align: []
@ -184,8 +184,8 @@
var needsValidation = document.querySelectorAll('.needs-validation') var needsValidation = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(needsValidation) Array.prototype.slice.call(needsValidation)
.forEach(function(form) { .forEach(function (form) {
form.addEventListener('submit', function(event) { form.addEventListener('submit', function (event) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@ -209,7 +209,7 @@
const mainContainer = select('#main'); const mainContainer = select('#main');
if (mainContainer) { if (mainContainer) {
setTimeout(() => { setTimeout(() => {
new ResizeObserver(function() { new ResizeObserver(function () {
select('.echart', true).forEach(getEchart => { select('.echart', true).forEach(getEchart => {
echarts.getInstanceByDom(getEchart).resize(); echarts.getInstanceByDom(getEchart).resize();
}) })
@ -217,4 +217,167 @@
}, 200); }, 200);
} }
/**
* Select all functionality
*/
var btnSelectAll = document.getElementById("SelectAllBTN");
var tableListCheckboxes = document.querySelectorAll(".deviceSelect");
function itemListCheckChanged(event) {
let isAllChecked = Array.from(tableListCheckboxes).map(itm => itm.checked);
if (isAllChecked.every(bool => bool == true)) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
} else if (isAllChecked.every(bool => bool == false)) {
btnSelectAll.checked = false;
btnSelectAll.indeterminate = false;
} else {
btnSelectAll.indeterminate = true;
}
}
tableListCheckboxes.forEach(item => {
item.addEventListener("click", itemListCheckChanged);
})
btnSelectAll.addEventListener("click", event => {
let checkedState = event.target.checked;
tableListCheckboxes.forEach(ckeckbox => ckeckbox.checked = checkedState);
})
/**
* Avoid hide dropdown when user clicked inside
*/
document.getElementById("dropDownLotsSelector").addEventListener("click", event => {
event.stopPropagation();
})
/**
* Search form functionality
*/
window.addEventListener("DOMContentLoaded", () => {
var searchForm = document.getElementById("SearchForm")
var inputSearch = document.querySelector("#SearchForm > input")
var doSearch = true
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
})
let timeoutHandler = setTimeout(() => { }, 1)
let dropdownList = document.getElementById("dropdown-search-list")
let defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
inputSearch.addEventListener("input", (e) => {
clearTimeout(timeoutHandler)
let searchText = e.target.value
if (searchText == '') {
document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
return
}
let resultCount = 0;
function searchCompleted() {
resultCount++;
setTimeout(() => {
if (resultCount == 2 && document.getElementById("dropdown-search-list").children.length == 2) {
document.getElementById("dropdown-search-list").innerHTML = `
<li id="deviceSearchLoader" class="dropdown-item">
<i class="bi bi-x-lg"></i>
<span style="margin-right: 10px">Nothing found</span>
</li>`
}
}, 100)
}
timeoutHandler = setTimeout(async () => {
dropdownList.innerHTML = `
<li id="deviceSearchLoader" class="dropdown-item">
<i class="bi bi-laptop"></i>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</li>
<li id="lotSearchLoader" class="dropdown-item">
<i class="bi bi-folder2"></i>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</li>`;
try {
Api.search_device(searchText.toUpperCase()).then(devices => {
dropdownList.querySelector("#deviceSearchLoader").style = "display: none"
for (let i = 0; i < devices.length; i++) {
const device = devices[i];
// See: ereuse_devicehub/resources/device/models.py
var verboseName = `${device.type} ${device.manufacturer} ${device.model}`
const templateString = `
<li>
<a class="dropdown-item" href="${API_URLS.devices_detail.replace("ReplaceTEXT", device.devicehubID)}" style="display: flex; align-items: center;" href="#">
<i class="bi bi-laptop"></i>
<span style="margin-right: 10px">${verboseName}</span>
<span class="badge bg-secondary" style="margin-left: auto;">${device.devicehubID}</span>
</a>
</li>`;
dropdownList.innerHTML += templateString
if (i == 4) { // Limit to 4 resullts
break;
}
}
searchCompleted();
})
} catch (error) {
dropdownList.innerHTML += `
<li id="deviceSearchLoader" class="dropdown-item">
<i class="bi bi-x"></i>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Error searching devices</span>
</div>
</li>`;
console.log(error);
}
try {
Api.get_lots().then(lots => {
dropdownList.querySelector("#lotSearchLoader").style = "display: none"
for (let i = 0; i < lots.length; i++) {
const lot = lots[i];
if (lot.name.toUpperCase().includes(searchText.toUpperCase())) {
const templateString = `
<li>
<a class="dropdown-item" href="${API_URLS.lots_detail.replace("ReplaceTEXT", lot.id)}" style="display: flex; align-items: center;" href="#">
<i class="bi bi-folder2"></i>
<span style="margin-right: 10px">${lot.name}</span>
</a>
</li>`;
dropdownList.innerHTML += templateString
if (i == 4) { // Limit to 4 resullts
break;
}
}
}
searchCompleted();
})
} catch (error) {
dropdownList.innerHTML += `
<li id="deviceSearchLoader" class="dropdown-item">
<i class="bi bi-x"></i>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Error searching lots</span>
</div>
</li>`;
console.log(error);
}
}, 1000)
})
})
})(); })();

View File

@ -180,3 +180,205 @@ function export_file(type_file) {
$("#exportAlertModal").click(); $("#exportAlertModal").click();
} }
} }
/**
* Reactive lots button
*/
async function processSelectedDevices() {
class Actions {
constructor() {
this.list = []; // list of petitions of requests @item --> {type: ["Remove" | "Add"], "LotID": string, "devices": number[]}
}
/**
* Manage the actions that will be performed when applying the changes
* @param {*} ev event (Should be a checkbox type)
* @param {string} lotID lot id
* @param {number} deviceID device id
*/
manage(event, lotID, deviceListID) {
event.preventDefault();
const indeterminate = event.srcElement.indeterminate;
const checked = !event.srcElement.checked;
var found = this.list.filter(list => list.lotID == lotID)[0];
var foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1;
if (checked) {
if (found != undefined && found.type == "Remove") {
if (found.isFromIndeterminate == true) {
found.type = "Add";
this.list[foundIndex] = found;
} else {
this.list = this.list.filter(list => list.lotID != lotID);
}
} else {
this.list.push({ type: "Add", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
}
} else {
if (found != undefined && found.type == "Add") {
if (found.isFromIndeterminate == true) {
found.type = "Remove";
this.list[foundIndex] = found;
} else {
this.list = this.list.filter(list => list.lotID != lotID);
}
} else {
this.list.push({ type: "Remove", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
}
}
if (this.list.length > 0) {
document.getElementById("ApplyDeviceLots").classList.remove("disabled");
} else {
document.getElementById("ApplyDeviceLots").classList.add("disabled");
}
}
/**
* Creates notification to give feedback to user
* @param {string} title notification title
* @param {string | null} toastText notification text
* @param {boolean} isError defines if a toast is a error
*/
notifyUser(title, toastText, isError) {
let toast = document.createElement("div");
toast.classList = "alert alert-dismissible fade show " + (isError ? "alert-danger" : "alert-success");
toast.attributes["data-autohide"] = !isError;
toast.attributes["role"] = "alert";
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>`;
if (toastText && toastText.length > 0) {
toast.innerHTML += `<br>${toastText}`;
}
document.getElementById("NotificationsContainer").appendChild(toast);
if (!isError) {
setTimeout(() => toast.classList.remove("show"), 3000);
}
setTimeout(() => document.getElementById("NotificationsContainer").innerHTML == "", 3500);
}
/**
* Get actions and execute call request to add or remove devices from lots
*/
doActions() {
var requestCount = 0; // This is for count all requested api count, to perform reRender of table device list
this.list.forEach(async action => {
if (action.type == "Add") {
try {
await Api.devices_add(action.lotID, action.devices);
this.notifyUser("Devices sucefully aded to selected lot/s", "", false);
} catch (error) {
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
}
} else if (action.type == "Remove") {
try {
await Api.devices_remove(action.lotID, action.devices);
this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
} catch (error) {
this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true);
}
}
requestCount += 1
if (requestCount == this.list.length) {
this.reRenderTable();
this.list = [];
}
})
document.getElementById("dropDownLotsSelector").classList.remove("show");
}
/**
* Re-render list in table
*/
async reRenderTable() {
var newRequest = await Api.doRequest(window.location)
var tmpDiv = document.createElement("div")
tmpDiv.innerHTML = newRequest
var 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)
for (let i = 0; i < oldTable.length; i++) {
if (!newTable.includes(oldTable[i])) {
// variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411)
table.rows().remove(i)
}
}
}
}
var eventClickActions;
/**
* Generates a list item with a correspondient checkbox state
* @param {String} lotID
* @param {String} lotName
* @param {Array<number>} selectedDevicesIDs
* @param {HTMLElement} target
*/
function templateLot(lotID, lot, selectedDevicesIDs, elementTarget, actions) {
elementTarget.innerHTML = ""
var htmlTemplate = `<input class="form-check-input" type="checkbox" id="${lotID}" style="width: 20px; height: 20px; margin-right: 7px;">
<label class="form-check-label" for="${lotID}">${lot.name}</label>`;
var existLotList = selectedDevicesIDs.map(selected => lot.devices.includes(selected));
var doc = document.createElement('li');
doc.innerHTML = htmlTemplate;
if (selectedDevicesIDs.length <= 0) {
doc.children[0].disabled = true;
} else if (existLotList.every(value => value == true)) {
doc.children[0].checked = true;
} else if (existLotList.every(value => value == false)) {
doc.children[0].checked = false;
} else {
doc.children[0].indeterminate = true;
}
doc.children[0].addEventListener('mouseup', (ev) => actions.manage(ev, lotID, selectedDevicesIDs));
elementTarget.append(doc);
}
var listHTML = $("#LotsSelector")
// Get selected devices
var selectedDevicesIDs = $.map($(".deviceSelect").filter(':checked'), function (x) { return parseInt($(x).attr('data')) });
if (selectedDevicesIDs.length <= 0) {
listHTML.html('<li style="color: red; text-align: center">No devices selected</li>');
return;
}
// Initialize Actions list, and set checkbox triggers
var actions = new Actions();
if (eventClickActions) {
document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions);
}
eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => actions.doActions());
document.getElementById("ApplyDeviceLots").classList.add("disabled");
try {
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);
var lots = await Api.get_lots();
lots = lots.map(lot => {
lot.devices = devices
.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0)
.map(device => parseInt(device.id));
return lot;
})
listHTML.html('');
lots.forEach(lot => templateLot(lot.id, lot, selectedDevicesIDs, listHTML, actions));
} catch (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>');
}
}

View File

@ -5,8 +5,8 @@ $(document).ready(function() {
load_size(); load_size();
}) })
function qr_draw(url) { function qr_draw(url, id) {
var qrcode = new QRCode($("#qrcode")[0], { var qrcode = new QRCode($(id)[0], {
text: url, text: url,
width: 128, width: 128,
height: 128, height: 128,
@ -54,16 +54,26 @@ function printpdf() {
var border = 2; var border = 2;
var height = parseInt($("#height-tag").val()); var height = parseInt($("#height-tag").val());
var width = parseInt($("#width-tag").val()); var width = parseInt($("#width-tag").val());
var tag = $("#tag").text();
var pdf = new jsPDF('l', 'mm', [width, height]);
var imgData = $('#qrcode img').attr("src");
img_side = Math.min(height, width) - 2*border; 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;
}; };
min_tag_side = (Math.min(height, width)/2) + border; min_tag_side = (Math.min(height, width)/2) + border;
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side); var last_tag_code = '';
pdf.text(tag, max_tag_side, min_tag_side);
pdf.save('Tag_'+tag+'.pdf'); var pdf = new jsPDF('l', 'mm', [width, height]);
$(".tag").map(function(x, y) {
if (x != 0){
pdf.addPage();
console.log(x)
};
var tag = $(y).text();
last_tag_code = tag;
var imgData = $('#'+tag+' img').attr("src");
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side);
pdf.text(tag, max_tag_side, min_tag_side);
});
pdf.save('Tag_'+last_tag_code+'.pdf');
} }

View File

@ -50,6 +50,20 @@
<!-- Template Main JS File --> <!-- Template Main JS File -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script>
<!-- Api backend -->
<script>
const API_URLS = {
Auth_Token: `Basic ${btoa("{{ current_user.token }}:")}`, //
currentUserID: "{{ current_user.id }}",
lots: "{{ url_for('Lot.main') }}",
lots_detail: "{{ url_for('inventory.lotdevicelist', lot_id='ReplaceTEXT') }}",
devices: "{{ url_for('Device.main') }}",
devices_modify: "{{ url_for('Lot.lot-device', id='UUID') }}",
devices_detail: "{{ url_for('inventory.device_details', id='ReplaceTEXT')}}"
}
</script>
<script src="{{ url_for('static', filename='js/api.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -12,9 +12,21 @@
</div><!-- End Logo --> </div><!-- End Logo -->
<div class="search-bar"> <div class="search-bar">
<form class="search-form d-flex align-items-center" method="POST" action="#"> <form class="search-form d-flex align-items-center" method="" id="SearchForm" action="#">
<input type="text" name="query" placeholder="Search" title="Enter search keyword"> <input class="dropdown-toggle" type="text" name="query" placeholder="Search" title="Enter search keyword"
autocomplete="off" id="dropdownSearch" data-bs-toggle="dropdown" aria-expanded="false">
<button type="submit" title="Search"><i class="bi bi-search"></i></button> <button type="submit" title="Search"><i class="bi bi-search"></i></button>
<ul class="dropdown-menu" autoClose="outside" aria-labelledby="dropdownSearch" id="dropdown-search-list"
style="min-width: 100px;">
<li class="dropdown-header">
<h6 class="dropdown-header">You can search:</h6>
</li>
<li class="dropdown-item"><i class="bi bi-laptop"></i> Devices <span class="badge bg-secondary"
style="float: right;">DHID</span></li>
<li class="dropdown-item"><i class="bi bi-folder2"></i> lots <span class="badge bg-secondary"
style="float: right;">Name</span></li>
</ul>
</form> </form>
</div><!-- End Search Bar --> </div><!-- End Search Bar -->
@ -53,7 +65,7 @@
</li> </li>
<li> <li>
<a class="dropdown-item d-flex align-items-center" href="pages-faq.html"> <a class="dropdown-item d-flex align-items-center" href="https://help.usody.com/" target="_blank">
<i class="bi bi-question-circle"></i> <i class="bi bi-question-circle"></i>
<span>Need Help?</span> <span>Need Help?</span>
</a> </a>
@ -101,87 +113,88 @@
<li class="nav-item"> <li class="nav-item">
{% if lot and lot.is_incoming %} {% if lot and lot.is_incoming %}
<a class="nav-link" data-bs-target="#incoming-lots-nav" data-bs-toggle="collapse" href="#"> <a class="nav-link" data-bs-target="#incoming-lots-nav" data-bs-toggle="collapse" href="#">
{% else %} {% else %}
<a class="nav-link collapsed" data-bs-target="#incoming-lots-nav" data-bs-toggle="collapse" href="#"> <a class="nav-link collapsed" data-bs-target="#incoming-lots-nav" data-bs-toggle="collapse" href="#">
{% endif %} {% endif %}
<i class="bi bi-arrow-down-right"></i><span>Incoming Lots</span><i class="bi bi-chevron-down ms-auto"></i> <i class="bi bi-arrow-down-right"></i><span>Incoming Lots</span><i class="bi bi-chevron-down ms-auto"></i>
</a> </a>
{% if lot and lot.is_incoming %} {% if lot and lot.is_incoming %}
<ul id="incoming-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav"> <ul id="incoming-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
{% else %} {% else %}
<ul id="incoming-lots-nav" class="nav-content collapse" data-bs-parent="#sidebar-nav"> <ul id="incoming-lots-nav" class="nav-content collapse" data-bs-parent="#sidebar-nav">
{% endif %} {% endif %}
{% for lot in lots %} {% for lot in lots %}
{% if lot.is_incoming %} {% if lot.is_incoming %}
<li> <li>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}"> <a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<i class="bi bi-circle"></i><span>{{ lot.name }}</span> <i class="bi bi-circle"></i><span>{{ lot.name }}</span>
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</li><!-- End Incoming Lots Nav --> </li><!-- End Incoming Lots Nav -->
<li class="nav-item"> <li class="nav-item">
{% if lot and lot.is_outgoing %} {% if lot and lot.is_outgoing %}
<a class="nav-link" data-bs-target="#outgoing-lots-nav" data-bs-toggle="collapse" href="#"> <a class="nav-link" data-bs-target="#outgoing-lots-nav" data-bs-toggle="collapse" href="#">
{% else %} {% else %}
<a class="nav-link collapsed" data-bs-target="#outgoing-lots-nav" data-bs-toggle="collapse" href="#"> <a class="nav-link collapsed" data-bs-target="#outgoing-lots-nav" data-bs-toggle="collapse" href="#">
{% endif %} {% endif %}
<i class="bi bi-arrow-up-right"></i><span>Outgoing Lots</span><i class="bi bi-chevron-down ms-auto"></i> <i class="bi bi-arrow-up-right"></i><span>Outgoing Lots</span><i class="bi bi-chevron-down ms-auto"></i>
</a> </a>
{% if lot and lot.is_outgoing %} {% if lot and lot.is_outgoing %}
<ul id="outgoing-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav"> <ul id="outgoing-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
{% else %} {% else %}
<ul id="outgoing-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav"> <ul id="outgoing-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
{% endif %} {% endif %}
{% for lot in lots %} {% for lot in lots %}
{% if lot.is_outgoing %} {% if lot.is_outgoing %}
<li> <li>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}"> <a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<i class="bi bi-circle"></i><span>{{ lot.name }}</span> <i class="bi bi-circle"></i><span>{{ lot.name }}</span>
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</li><!-- End Outgoing Lots Nav --> </li><!-- End Outgoing Lots Nav -->
<li class="nav-item"> <li class="nav-item">
{% if lot and lot.is_temporary %} {% if lot and lot.is_temporary %}
<a class="nav-link" data-bs-target="#temporal-lots-nav" data-bs-toggle="collapse" href="#"> <a class="nav-link" data-bs-target="#temporal-lots-nav" data-bs-toggle="collapse" href="#">
{% else %} {% else %}
<a class="nav-link collapsed" data-bs-target="#temporal-lots-nav" data-bs-toggle="collapse" href="#"> <a class="nav-link collapsed" data-bs-target="#temporal-lots-nav" data-bs-toggle="collapse" href="#">
{% endif %} {% endif %}
<i class="bi bi-layout-text-window-reverse"></i><span>Temporary Lots</span><i class="bi bi-chevron-down ms-auto"></i> <i class="bi bi-layout-text-window-reverse"></i><span>Temporary Lots</span><i
</a> class="bi bi-chevron-down ms-auto"></i>
{% if lot and lot.is_temporary %} </a>
<ul id="temporal-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav"> {% if lot and lot.is_temporary %}
{% else %} <ul id="temporal-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
<ul id="temporal-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav"> {% else %}
{% endif %} <ul id="temporal-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
<li> {% endif %}
<a href="{{ url_for('inventory.lot_add')}}"> <li>
<i class="bi bi-plus" style="font-size: larger;"></i><span>New temporary lot</span> <a href="{{ url_for('inventory.lot_add')}}">
</a> <i class="bi bi-plus" style="font-size: larger;"></i><span>New temporary lot</span>
</li> </a>
{% for lot in lots %} </li>
{% if lot.is_temporary %} {% for lot in lots %}
<li> {% if lot.is_temporary %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}"> <li>
<i class="bi bi-circle"></i><span>{{ lot.name }}</span> <a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
</a> <i class="bi bi-circle"></i><span>{{ lot.name }}</span>
</li> </a>
{% endif %} </li>
{% endfor %} {% endif %}
</ul> {% endfor %}
</ul>
</li><!-- End Temporal Lots Nav --> </li><!-- End Temporal Lots Nav -->
<li class="nav-heading">Utils</li> <li class="nav-heading">Utils</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.taglist')}}"> <a class="nav-link collapsed" href="{{ url_for('labels.label_list')}}">
<i class="bi bi-tags"></i> <i class="bi bi-tags"></i>
<span>Tags</span> <span>Tags</span>
</a> </a>
@ -193,13 +206,18 @@
<main id="main" class="main"> <main id="main" class="main">
{% block messages %} {% block messages %}
{% for level, message in get_flashed_messages(with_categories=true) %} {% for level, message in get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{ level}} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ level}} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i> {% if '_message_icon' in session %}
{{ message }} <i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> {% else %}
</div> <!-- fallback if 3rd party libraries (e.g. flask_login.login_required) -->
{% endfor %} <i class="bi bi-info-circle me-1"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endblock %} {% endblock %}
{% block main %} {% block main %}

View File

@ -8,7 +8,6 @@
<h1>Profile</h1> <h1>Profile</h1>
<nav> <nav>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
<li class="breadcrumb-item">Users</li> <li class="breadcrumb-item">Users</li>
<li class="breadcrumb-item active">Profile</li> <li class="breadcrumb-item active">Profile</li>
</ol> </ol>
@ -28,7 +27,7 @@
</div> </div>
<div class="col-xl-8"> <div class="col-xl-8 d-none"><!-- TODO (hidden until is implemented )-->
<div class="card"> <div class="card">
<div class="card-body pt-3"> <div class="card-body pt-3">

View File

@ -1,33 +0,0 @@
<div class="modal fade" id="addingLotModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Adding to a lot</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.lot_devices_add') }}" method="post">
{{ form_lot_device.csrf_token }}
<div class="modal-body">
Please write a name of a lot
<select class="form-control selectpicker" id="selectLot" name="lot" data-live-search="true">
{% for lot in lots %}
<option value="{{ lot.id }}">{{ lot.name }}</option>
{% endfor %}
</select>
<input class="devicesList" type="hidden" name="devices" />
<p class="text-danger pol">
You need select first some device for adding this in a lot
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" style="display: none;" value="Save changes" />
</div>
</form>
</div>
</div>
</div>

View File

@ -38,15 +38,15 @@
<div><!-- lot actions --> <div><!-- 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> <span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
<a class="me-2" href="javascript:removeLot()">
<i class="bi bi-trash"></i> Remove Lot
</a>
<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 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>
<a class="text-danger" href="javascript:removeLot()">
<i class="bi bi-trash"></i> Delete Lot
</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -71,25 +71,22 @@
{% endif %} {% endif %}
<div class="tab-content pt-5"> <div class="tab-content pt-5">
<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>
<div class="btn-group dropdown ml-1"> <div class="btn-group dropdown ml-1">
<button id="btnLots" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnLots" type="button" onclick="processSelectedDevices()" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-folder2"></i> <i class="bi bi-folder2"></i>
Lots Lots
<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"> <ul class="dropdown-menu" aria-labelledby="btnLots" style="width: 300px;" id="dropDownLotsSelector">
<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>
<li><hr /></li>
<li> <li>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addingLotModal"> <a href="#" class="dropdown-item" id="ApplyDeviceLots">
<i class="bi bi-plus"></i> <i class="bi bi-check"></i>
Add selected Devices to a lot Apply
</a>
</li>
<li>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#removeLotModal">
<i class="bi bi-x"></i>
Remove selected devices from a lot
</a> </a>
</li> </li>
</ul> </ul>
@ -235,6 +232,17 @@
Remove Tag from selected Device Remove Tag from selected Device
</a> </a>
</li> </li>
<li>
<form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}">
{% for f in form_print_labels %}
{{ f }}
{% endfor %}
<a href="javascript:$('#print_labels').submit()" class="dropdown-item">
<i class="bi bi-printer"></i>
Print labels
</a>
</form>
</li>
</ul> </ul>
</div> </div>
@ -250,7 +258,7 @@
{% else %} {% else %}
<a href="{{ url_for('inventory.upload_snapshot') }}" class="dropdown-item"> <a href="{{ url_for('inventory.upload_snapshot') }}" class="dropdown-item">
{% endif %} {% endif %}
<i class="bi bi-plus"></i> <i class="bi bi-upload"></i>
Upload a new Snapshot Upload a new Snapshot
</a> </a>
</li> </li>
@ -335,7 +343,7 @@
</td> </td>
<td> <td>
{% for t in dev.tags | sort(attribute="id") %} {% for t in dev.tags | sort(attribute="id") %}
<a href="{{ url_for('inventory.tag_details', id=t.id)}}">{{ t.id }}</a> <a href="{{ url_for('labels.label_details', id=t.id)}}">{{ t.id }}</a>
{% if not loop.last %},{% endif %} {% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
</td> </td>
@ -383,13 +391,13 @@
</div> </div>
</div> </div>
<div id="NotificationsContainer" style="position: absolute; bottom: 0; right: 0; margin: 10px; margin-top: 70px; width: calc(100% - 310px);"></div>
</div> </div>
</div> </div>
</section> </section>
{% include "inventory/addDeviceslot.html" %}
{% include "inventory/addDevicestag.html" %} {% include "inventory/addDevicestag.html" %}
{% include "inventory/removeDeviceslot.html" %} {% include "inventory/lot_delete_modal.html" %}
{% include "inventory/removelot.html" %}
{% include "inventory/actions.html" %} {% include "inventory/actions.html" %}
{% include "inventory/allocate.html" %} {% include "inventory/allocate.html" %}
{% include "inventory/data_wipe.html" %} {% include "inventory/data_wipe.html" %}

View File

@ -3,21 +3,21 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Remove Lot</h5> <h5 class="modal-title">Delete Lot</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>
<div class="modal-body"> <div class="modal-body">
Are you sure that you want to remove lot <strong>{{ lot.name }}</strong>? Are you sure that you want to delete lot <strong>{{ lot.name }}</strong>?
<p class="text-danger"> <p class="text-danger">
There are devices in this lot, are you sure you want to delete it? This action cannot be undone.
</p> </p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary-outline" data-bs-dismiss="modal">Cancel</button>
<a href="{{ url_for('inventory.lot_del', id=lot.id)}}" type="button" class="btn btn-primary"> <a href="{{ url_for('inventory.lot_del', id=lot.id)}}" type="button" class="btn btn-danger">
Confirm Delete it!
</a> </a>
</div> </div>

View File

@ -1,32 +0,0 @@
<div class="modal fade" id="removeLotModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Remove from lot</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.lot_devices_del') }}" method="post">
{{ form_lot_device.csrf_token }}
<div class="modal-body">
Please write a name of a lot
<select class="form-control selectpicker" id="selectLot" name="lot" data-live-search="true">
{% for lot in lots %}
<option value="{{ lot.id }}">{{ lot.name }}</option>
{% endfor %}
</select>
<input class="devicesList" type="hidden" name="devices" />
<p class="text-danger pol">
You need select first some device for remove this from a lot
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" style="display: none;" value="Save changes" />
</div>
</form>
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@
<h1>Inventory</h1> <h1>Inventory</h1>
<nav> <nav>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.taglist')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
<li class="breadcrumb-item active">Tag details {{ tag.id }}</li> <li class="breadcrumb-item active">Tag details {{ tag.id }}</li>
</ol> </ol>
</nav> </nav>
@ -40,17 +40,17 @@
</div> </div>
</div> </div>
<h5 class="card-title">Print details</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; 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">
<div id="qrcode"></div> <div id="{{ tag.id }}"></div>
</div> </div>
<div class="col"> <div class="col">
<div style="padding-top: 55px"><b id="tag">{{ tag.id }}</b></div> <div style="padding-top: 55px"><b class="tag">{{ tag.id }}</b></div>
</div> </div>
</div> </div>
</div> </div>
@ -109,6 +109,6 @@
<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">
qr_draw("{{url_for('inventory.device_details', id=tag.device.devicehub_id, _external=True)}}"); qr_draw("{{url_for('inventory.device_details', id=tag.device.devicehub_id, _external=True)}}", "#{{ tag.id }}");
</script> </script>
{% endblock main %} {% endblock main %}

View File

@ -20,7 +20,7 @@
<!-- Bordered Tabs --> <!-- Bordered Tabs -->
<div class="btn-group dropdown ml-1"> <div class="btn-group dropdown ml-1">
<a href="{{ url_for('inventory.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 Tag
<span class="caret"></span> <span class="caret"></span>
@ -28,7 +28,7 @@
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown ml-1" uib-dropdown="">
<a href="{{ url_for('inventory.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 Tag
<span class="caret"></span> <span class="caret"></span>
@ -52,7 +52,7 @@
<tbody> <tbody>
{% for tag in tags %} {% for tag in tags %}
<tr> <tr>
<td><a href="{{ url_for('inventory.tag_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 tag {% else %}Named tag{% endif %}</td>
<td>{{ tag.get_provider }}</td> <td>{{ tag.get_provider }}</td>
<td> <td>

View File

@ -0,0 +1,103 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Print Labels</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
<li class="breadcrumb-item active">Print Labels</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-8">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Print Labels</h5>
<p class="text-center small">{{ title }}</p>
</div>
<div class="row">
<div class="col-lg-3 col-md-4">
{% for tag in tags %}
<div style="width:256px; height:148px; border: solid 1px; padding: 10px;">
<div id="print">
<div class="row">
<div class="col">
<div id="{{ tag.id }}"></div>
</div>
<div class="col">
<div style="padding-top: 55px">
<b class="tag">{{ tag.id }}</b>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="col-1">
</div>
<div class="col label">
<label class="col-form-label col-sm-2">Size</label>
<div class="col-sm-10">
<div class="input-group mb-3">
<select class="form-select" id="printerType">
<option label="Brother small size (62 x 29)" value="brotherSmall" selected="selected">
Brother small size (62 x 29)
</option>
<option label="Printer tag small (97 x 59)" value="smallTagPrinter">
Printer tag small (97 x 59)
</option>
</select>
</div>
</div>
<label class="col-form-label col-sm-2">Width</label>
<div class="col-sm-10">
<div class="input-group mb-3">
<input class="form-control" id="width-tag" name='width-tag' type="number" value="62" min="52" max="300" />
<span class="input-group-text">mm</span>
</div>
</div>
<label class="col-form-label col-sm-2">Height</label>
<div class="col-sm-10">
<div class="input-group mb-3">
<input class="form-control" id="height-tag" name='height-tag' type="number" value="29" min="28" max="200" />
<span class="input-group-text">mm</span>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-lg-3 col-md-4">
<a href="javascript:printpdf()" class="btn btn-success">Print</a>
</div>
<div class="col-lg-3 col-md-4">
<a href="javascript:save_size()" class="btn btn-primary">Save</a>
</div>
<div class="col-lg-3 col-md-4">
<a href="javascript:reset_size()" class="btn btn-danger">Reset</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script src="{{ url_for('static', filename='js/qrcode.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 type="text/javascript">
{% for tag in tags %}
qr_draw("{{ url_for('inventory.device_details', id=tag.device.devicehub_id, _external=True) }}", "#{{ tag.id }}")
{% endfor %}
</script>
{% 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('inventory.taglist')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
<li class="breadcrumb-item">{{ page_title }}</li> <li class="breadcrumb-item">{{ page_title }}</li>
</ol> </ol>
</nav> </nav>
@ -49,7 +49,7 @@
</div> </div>
<div> <div>
<a href="{{ url_for('inventory.taglist') }}" class="btn btn-danger">Cancel</a> <a href="{{ url_for('labels.label_list') }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button> <button class="btn btn-primary" type="submit">Save</button>
</div> </div>
</form> </form>

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('inventory.taglist')}}">Tag management</a></li> <li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
<li class="breadcrumb-item">{{ page_title }}</li> <li class="breadcrumb-item">{{ page_title }}</li>
</ol> </ol>
</nav> </nav>
@ -49,7 +49,7 @@
</div> </div>
<div> <div>
<a href="{{ url_for('inventory.taglist') }}" class="btn btn-danger">Cancel</a> <a href="{{ url_for('labels.label_list') }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button> <button class="btn btn-primary" type="submit">Save</button>
</div> </div>
</form> </form>

View File

@ -11,6 +11,11 @@ from ereuse_devicehub.utils import is_safe_url
core = Blueprint('core', __name__) core = Blueprint('core', __name__)
@core.route("/")
def index():
return flask.redirect(flask.url_for('core.login'))
class LoginView(View): class LoginView(View):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
template_name = 'ereuse_devicehub/user_login.html' template_name = 'ereuse_devicehub/user_login.html'

View File

@ -8,15 +8,17 @@ from flask_wtf.csrf import CSRFProtect
from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.inventory.views import devices from ereuse_devicehub.inventory.views import devices
from ereuse_devicehub.labels.views import labels
from ereuse_devicehub.views import core from ereuse_devicehub.views import core
app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA) app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA)
app.register_blueprint(core) app.register_blueprint(core)
app.register_blueprint(devices) app.register_blueprint(devices)
app.register_blueprint(labels)
# configure & enable CSRF of Flask-WTF # configure & enable CSRF of Flask-WTF
# NOTE: enable by blueprint to exclude API views # NOTE: enable by blueprint to exclude API views
# TODO(@slamora: enable by default & exclude API views when decouple of Teal is completed # TODO(@slamora: enable by default & exclude API views when decouple of Teal is completed
csrf = CSRFProtect(app) csrf = CSRFProtect(app)
csrf.protect(core) # csrf.protect(core)
csrf.protect(devices) # csrf.protect(devices)

View File

@ -6,7 +6,7 @@ click==6.7
click-spinner==0.1.8 click-spinner==0.1.8
colorama==0.3.9 colorama==0.3.9
colour==0.1.5 colour==0.1.5
ereuse-utils[naming,test,session,cli]==0.4.0b49 ereuse-utils[naming,test,session,cli]==0.4.0b50
Flask==1.0.2 Flask==1.0.2
Flask-Cors==3.0.10 Flask-Cors==3.0.10
Flask-Login==0.5.0 Flask-Login==0.5.0
@ -15,6 +15,9 @@ Flask-WTF==1.0.0
hashids==1.2.0 hashids==1.2.0
inflection==0.3.1 inflection==0.3.1
itsdangerous==2.0.1 itsdangerous==2.0.1
# lock Jinja2 version because it's the latest compatible with Flask 1.0.X
# see related info on https://github.com/pallets/jinja/issues/1628
Jinja2==3.0.3
marshmallow==3.0.0b11 marshmallow==3.0.0b11
marshmallow-enum==1.4.1 marshmallow-enum==1.4.1
passlib==1.7.1 passlib==1.7.1

View File

@ -24,7 +24,7 @@ setup(
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
python_requires='>=3.7.3', python_requires='>=3.7.3',
long_description=Path('README.rst').read_text('utf8'), long_description=Path('README.md').read_text('utf8'),
install_requires=[ install_requires=[
'teal>=0.2.0a38', # teal always first 'teal>=0.2.0a38', # teal always first
'click', 'click',