Merge branch 'testing' into PR-lots-list

This commit is contained in:
Santiago L 2022-04-11 16:19:23 +02:00
commit 4065f46dd9
24 changed files with 696 additions and 397 deletions

View File

@ -8,6 +8,8 @@ ml).
## master ## master
## testing ## testing
- [changed] #211 Print DHID-QR label for selected devices.
- [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``.

54
development-setup.md Executable file
View File

@ -0,0 +1,54 @@
# 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
```
copy `examples/app.py` to project directory:
```bash
copy examples/app.py .
```
## 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,7 +29,8 @@ class Devicehub(Teal):
Dummy = Dummy Dummy = Dummy
jinja_environment = Environment jinja_environment = Environment
def __init__(self, def __init__(
self,
inventory: str, inventory: str,
config: DevicehubConfig = DevicehubConfig(), config: DevicehubConfig = DevicehubConfig(),
db: SQLAlchemy = db, db: SQLAlchemy = db,
@ -43,18 +44,34 @@ class Devicehub(Teal):
instance_path=None, instance_path=None,
instance_relative_config=False, instance_relative_config=False,
root_path=None, root_path=None,
Auth: Type[Auth] = Auth): Auth: Type[Auth] = Auth,
):
assert inventory assert inventory
super().__init__(config, db, inventory, import_name, static_url_path, static_folder, super().__init__(
config,
db,
inventory,
import_name,
static_url_path,
static_folder,
static_host, static_host,
host_matching, subdomain_matching, template_folder, instance_path, host_matching,
instance_relative_config, root_path, False, Auth) 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):
# 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) 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(
'--org-name',
'-on',
default='My Organization', default='My Organization',
help='The name of the default organization that owns this inventory.') help='The name of the default organization that owns this inventory.',
@click.option('--org-id', '-oi', )
default='foo-bar', @click.option(
help='The Tax ID of the organization.') '--org-id', '-oi', default='foo-bar', help='The Tax ID of the organization.'
@click.option('--tag-url', '-tu', )
@click.option(
'--tag-url',
'-tu',
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False), type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
default='http://example.com', default='http://example.com',
help='The base url (scheme and host) of the tag provider.') help='The base url (scheme and host) of the tag provider.',
@click.option('--tag-token', '-tt', )
@click.option(
'--tag-token',
'-tt',
type=click.UUID, type=click.UUID,
default='899c794e-1737-4cea-9232-fdc507ab7106', default='899c794e-1737-4cea-9232-fdc507ab7106',
help='The token provided by the tag provider. It is an UUID.') help='The token provided by the tag provider. It is an UUID.',
@click.option('--erase/--no-erase', )
@click.option(
'--erase/--no-erase',
default=False, default=False,
help='Delete the schema before? ' help='Delete the schema before? '
'If --common is set this includes the common database.') 'If --common is set this includes the common database.',
@click.option('--common/--no-common', )
@click.option(
'--common/--no-common',
default=False, default=False,
help='Creates common databases. Only execute if the database is empty.') help='Creates common databases. Only execute if the database is empty.',
def init_db(self, name: str, )
def init_db(
self,
name: str,
org_name: str, org_name: str,
org_id: str, org_id: str,
tag_url: boltons.urlutils.URL, tag_url: boltons.urlutils.URL,
tag_token: uuid.UUID, tag_token: uuid.UUID,
erase: bool, erase: bool,
common: 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

@ -485,46 +485,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
@ -22,12 +21,11 @@ from ereuse_devicehub.inventory.forms import (
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
@ -118,6 +116,7 @@ class DeviceListMix(GenericMixView):
'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,
@ -315,91 +314,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]
@ -716,14 +630,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

@ -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;
var last_tag_code = '';
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.addImage(imgData, 'PNG', border, border, img_side, img_side);
pdf.text(tag, max_tag_side, min_tag_side); pdf.text(tag, max_tag_side, min_tag_side);
pdf.save('Tag_'+tag+'.pdf'); });
pdf.save('Tag_'+last_tag_code+'.pdf');
} }

View File

@ -53,7 +53,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>
@ -181,7 +181,7 @@
<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>
@ -195,7 +195,11 @@
{% 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">
{% if '_message_icon' in session %}
<i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i> <i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i>
{% else %}<!-- fallback if 3rd party libraries (e.g. flask_login.login_required) -->
<i class="bi bi-info-circle me-1"></i>
{% endif %}
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>

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

@ -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>
@ -231,6 +231,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>
@ -246,7 +257,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>
@ -331,7 +342,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>
@ -387,7 +398,7 @@
{% include "inventory/addDeviceslot.html" %} {% include "inventory/addDeviceslot.html" %}
{% include "inventory/addDevicestag.html" %} {% include "inventory/addDevicestag.html" %}
{% include "inventory/removeDeviceslot.html" %} {% include "inventory/removeDeviceslot.html" %}
{% include "inventory/removelot.html" %} {% include "inventory/lot_delete_modal.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

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