Merge pull request #191 from eReuse/feature/server-side-render

Server side render - devicehub v2.0.0-alpha
This commit is contained in:
Santiago L 2022-03-15 14:59:01 +01:00 committed by GitHub
commit beb26ca232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 76679 additions and 52 deletions

View File

@ -45,11 +45,8 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -qy
sudo apt-get -y install postgresql-client
sudo apt-get -y install postgresql-client --no-install-recommends
python -m pip install --upgrade pip
pip install virtualenv
virtualenv env
source env/bin/activate
pip install flake8 pytest coverage
pip install -r requirements.txt
@ -65,10 +62,21 @@ jobs:
psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION citext SCHEMA public;"
psql -h "localhost" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "CREATE EXTENSION pg_trgm SCHEMA public;"
- name: Lint with flake8
run: |
# stop the build if:
# - E9,F63,F7,F82: Python syntax errors or undefined names
# - E501: line longer than 120 characters
# - C901: complexity greater than 10
# - F401: modules imported but unused
# See: https://flake8.pycqa.org/en/latest/user/error-codes.html
flake8 . --select=E9,F63,F7,F82,E501,C901,F401
flake8 . --exit-zero
- name: Run Tests
run: |
source env/bin/activate
coverage run --source='ereuse_devicehub' env/bin/pytest -m mvp --maxfail=5 tests/
export SECRET_KEY=`python3 -c 'import secrets; print(secrets.token_hex())'`
coverage run --source='ereuse_devicehub' -m pytest -m mvp --maxfail=5 tests/
coverage report --include='ereuse_devicehub/*'
coverage xml

View File

@ -6,27 +6,38 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
ml).
## master
[1.0.12-beta]
## testing
[1.0.13-beta]
## [1.0.13-beta]
## [2.0.0-alpha]
First server render HTML version. Completely rewrites views of angular JS client on flask.
- [added] #193 render on backend devices and lots
- [added] #195 render on backend tags system
- [added] #196 render on backend action system
- [added] #201 render on backend Data Wipe action
- [added] #203 render on backend Trade action
- [added] #204 render on backend export files
- [added] #205 UX improvements
- [added] #208 render on backend filter for type of devices in the general list
- [changed] #191 pass to drop teal and use the pure flask and use render from flask
- [changed] #207 Create automatic tag only for Computers.
- [changed] #209 adding a new device in a lot if it is created from a lot
- [fixed] #206 fix 2 bugs about visibility devices when you are not the owner
## [1.0.12-beta]
- [changes] #187 now is possible duplicate slots of RAM.
- [changes] #188 Excel report devices allow to see device to old owners.
- [changed] #187 now is possible duplicate slots of RAM.
- [changed] #188 Excel report devices allow to see device to old owners.
## [1.0.11-beta]
- [addend] #186 adding property power_on_hours.
- [added] #186 adding property power_on_hours.
## [1.0.10-beta]
- [addend] #170 can delete/deactivate devices.
- [bugfix] #168 can to do a trade without devices.
- [added] #170 can delete/deactivate devices.
- [added] #167 new actions of status devices: use, recycling, refurbish and management.
- [changes] #177 new structure of trade.
- [bugfix] #184 clean nested of schemas of lot
- [added] #182 adding power on hours
- [changed] #177 new structure of trade.
- [fixed] #168 can to do a trade without devices.
- [fixed] #184 clean nested of schemas of lot
## [1.0.9-beta]
- [added] #159 external document as proof of erase of disk
@ -34,7 +45,7 @@ ml).
## [1.0.8-beta]
- [bugfix] #161 fixing DataStorage with bigInteger
- [fixed] #161 fixing DataStorage with bigInteger
## [1.0.7-beta]
- [added] #158 support for encrypted snapshots data
@ -42,26 +53,26 @@ ml).
- [added] #140 adding endpoint for download the settings for usb workbench
## [1.0.6-beta]
- [bugfix] #143 biginteger instead of integer in TestDataStorage
- [fixed] #143 biginteger instead of integer in TestDataStorage
## [1.0.5-beta]
- [added] #124 adding endpoint for extract the internal stats of use
- [added] #122 system for verify all documents that it's produced from devicehub
- [added] #127 add one code for every named tag
- [added] #131 add one code for every device
- [bugfix] #138 search device with devicehubId
- [fixed] #138 search device with devicehubId
## [1.0.4-beta]
- [added] #95 adding endpoint for check the hash of one report
- [added] #98 adding endpoint for insert a new live
- [added] #98 adding endpoint for get all licences in one query
- [added] #102 adding endpoint for download metrics
- [bugfix] #100 fixing bug of scheme live
- [bugfix] #101 fixing bug when 2 users have one device and launch one live
- [changes] #114 clean blockchain of all models
- [changes] #118 deactivate manual merge
- [changes] #118 clean datas of public information of devices
- [remove] #114 remove proof system
- [changed] #114 clean blockchain of all models
- [changed] #118 deactivate manual merge
- [changed] #118 clean datas of public information of devices
- [fixed] #100 fixing bug of scheme live
- [fixed] #101 fixing bug when 2 users have one device and launch one live
- [removed] #114 remove proof system
## [1.0.3-beta]
- [added] #85 add mac of network adapter to device hid
@ -69,6 +80,6 @@ ml).
## [1.0.2-beta]
- [added] #87 allocate, deallocate and live actions
- [fixed] #89 save json on disk only for shapshots
- [added] #83 add owner_id in all kind of device
- [fixed] #89 save json on disk only for shapshots
- [fixed] #91 The most old time allow is 1970-01-01

View File

@ -1 +1 @@
__version__ = "1.0.12-beta"
__version__ = "2.0.0-alpha"

View File

@ -1,8 +1,8 @@
import os
import click.testing
import ereuse_utils
import flask.cli
import ereuse_utils
from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub
@ -11,7 +11,7 @@ import sys
sys.ps1 = '\001\033[92m\002>>> \001\033[0m\002'
sys.ps2= '\001\033[94m\002... \001\033[0m\002'
import os, readline, rlcompleter, atexit
import os, readline, atexit
history_file = os.path.join(os.environ['HOME'], '.python_history')
try:
readline.read_history_file(history_file)

View File

@ -35,6 +35,7 @@ class DevicehubConfig(Config):
import_resource(metric_def),
),)
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
SECRET_KEY = config('SECRET_KEY')
DB_USER = config('DB_USER', 'dhub')
DB_PASSWORD = config('DB_PASSWORD', 'ereuse')
DB_HOST = config('DB_HOST', 'localhost')

View File

@ -7,7 +7,8 @@ import click
import click_spinner
import ereuse_utils.cli
from ereuse_utils.session import DevicehubClient
from flask.globals import _app_ctx_stack, g
from flask import _app_ctx_stack, g
from flask_login import LoginManager, current_user
from flask_sqlalchemy import SQLAlchemy
from teal.db import SchemaSQLAlchemy
from teal.teal import Teal
@ -19,6 +20,7 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.dummy.dummy import Dummy
from ereuse_devicehub.resources.device.search import DeviceSearch
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.templating import Environment
@ -61,6 +63,17 @@ class Devicehub(Teal):
inv.command('search')(self.regenerate_search)
self.before_request(self._prepare_request)
self.configure_extensions()
def configure_extensions(self):
# configure Flask-Login
login_manager = LoginManager()
login_manager.init_app(self)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(user_id)
# noinspection PyMethodOverriding
@click.option('--name', '-n',
default='Test 1',
@ -150,6 +163,9 @@ class Devicehub(Teal):
inv = g.inventory = Inventory.current # type: Inventory
g.tag_provider = DevicehubClient(base_url=inv.tag_provider,
token=DevicehubClient.encode_token(inv.tag_token))
# NOTE: models init methods expects that current user is
# available on g.user (e.g. to initialize object owner)
g.user = current_user
def create_client(self, email='user@dhub.com', password='1234'):
client = UserClient(self, email, password, response_wrapper=self.response_class)

61
ereuse_devicehub/forms.py Normal file
View File

@ -0,0 +1,61 @@
from flask_wtf import FlaskForm
from werkzeug.security import generate_password_hash
from wtforms import BooleanField, EmailField, PasswordField, validators
from ereuse_devicehub.resources.user.models import User
class LoginForm(FlaskForm):
email = EmailField('Email Address', [validators.Length(min=6, max=35)])
password = PasswordField('Password', [validators.DataRequired()])
remember = BooleanField('Remember me')
error_messages = {
'invalid_login': (
"Please enter a correct email and password. Note that both "
"fields may be case-sensitive."
),
'inactive': "This account is inactive.",
}
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
email = self.email.data
password = self.password.data
self.user_cache = self.authenticate(email, password)
if self.user_cache is None:
self.form_errors.append(self.error_messages['invalid_login'])
return False
return self.confirm_login_allowed(self.user_cache)
def authenticate(self, email, password):
if email is None or password is None:
return
user = User.query.filter_by(email=email).first()
if user is None:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
generate_password_hash(password)
else:
if user.check_password(password):
return user
def confirm_login_allowed(self, user):
"""
Controls whether the given User may log in. This is a policy setting,
independent of end-user authentication. This default behavior is to
allow login by active users, and reject login by inactive users.
If the given user cannot log in, this method should raise a
``ValidationError``.
If the given user may log in, this method should return None.
"""
if not user.is_active:
self.form_errors.append(self.error_messages['inactive'])
return user.is_active

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,736 @@
import csv
import logging
from io import StringIO
import flask
import flask_weasyprint
from flask import Blueprint, g, make_response, request, url_for
from flask.views import View
from flask_login import current_user, login_required
from requests.exceptions import ConnectionError
from sqlalchemy import or_
from werkzeug.exceptions import NotFound
from ereuse_devicehub import __version__, messages
from ereuse_devicehub.db import db
from ereuse_devicehub.inventory.forms import (
AllocateForm,
DataWipeForm,
FilterForm,
LotDeviceForm,
LotForm,
NewActionForm,
NewDeviceForm,
TagDeviceForm,
TagForm,
TagUnnamedForm,
TradeDocumentForm,
TradeForm,
UploadSnapshotForm,
)
from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag
devices = Blueprint('inventory', __name__, url_prefix='/inventory')
logger = logging.getLogger(__name__)
class GenericMixView(View):
def get_lots(self):
return (
Lot.query.outerjoin(Trade)
.filter(
or_(
Trade.user_from == g.user,
Trade.user_to == g.user,
Lot.owner_id == g.user.id,
)
)
.distinct()
)
class DeviceListMix(GenericMixView):
decorators = [login_required]
template_name = 'inventory/device_list.html'
def get_context(self, lot_id):
form_filter = FilterForm()
filter_types = form_filter.search()
lots = self.get_lots()
lot = None
tags = (
Tag.query.filter(Tag.owner_id == current_user.id)
.filter(Tag.device_id.is_(None))
.order_by(Tag.id.asc())
)
if lot_id:
lot = lots.filter(Lot.id == lot_id).one()
devices = lot.devices
if "All" not in filter_types:
devices = [dev for dev in lot.devices if dev.type in filter_types]
devices = sorted(devices, key=lambda x: x.updated, reverse=True)
form_new_action = NewActionForm(lot=lot.id)
form_new_allocate = AllocateForm(lot=lot.id)
form_new_datawipe = DataWipeForm(lot=lot.id)
form_new_trade = TradeForm(
lot=lot.id,
user_to=g.user.email,
user_from=g.user.email,
)
else:
if "All" in filter_types:
devices = (
Device.query.filter(Device.owner_id == current_user.id)
.filter_by(lots=None)
.order_by(Device.updated.desc())
)
else:
devices = (
Device.query.filter(Device.owner_id == current_user.id)
.filter_by(lots=None)
.filter(Device.type.in_(filter_types))
.order_by(Device.updated.desc())
)
form_new_action = NewActionForm()
form_new_allocate = AllocateForm()
form_new_datawipe = DataWipeForm()
form_new_trade = ''
action_devices = form_new_action.devices.data
list_devices = []
if action_devices:
list_devices.extend([int(x) for x in action_devices.split(",")])
self.context = {
'devices': devices,
'lots': lots,
'form_lot_device': LotDeviceForm(),
'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe,
'form_new_trade': form_new_trade,
'form_filter': form_filter,
'lot': lot,
'tags': tags,
'list_devices': list_devices,
'version': __version__,
}
return self.context
class DeviceListView(DeviceListMix):
def dispatch_request(self, lot_id=None):
self.get_context(lot_id)
return flask.render_template(self.template_name, **self.context)
class DeviceDetailView(GenericMixView):
decorators = [login_required]
template_name = 'inventory/device_detail.html'
def dispatch_request(self, id):
lots = self.get_lots()
device = (
Device.query.filter(Device.owner_id == current_user.id)
.filter(Device.devicehub_id == id)
.one()
)
context = {
'device': device,
'lots': lots,
'page_title': 'Device {}'.format(device.devicehub_id),
'version': __version__,
}
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):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/lot.html'
title = "Add a new lot"
def dispatch_request(self):
form = LotForm()
if form.validate_on_submit():
form.save()
next_url = url_for('inventory.lotdevicelist', lot_id=form.id)
return flask.redirect(next_url)
lots = self.get_lots()
context = {
'form': form,
'title': self.title,
'lots': lots,
'version': __version__,
}
return flask.render_template(self.template_name, **context)
class LotUpdateView(View):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/lot.html'
title = "Edit a new lot"
def dispatch_request(self, id):
form = LotForm(id=id)
if form.validate_on_submit():
form.save()
next_url = url_for('inventory.lotdevicelist', lot_id=id)
return flask.redirect(next_url)
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {
'form': form,
'title': self.title,
'lots': lots,
'version': __version__,
}
return flask.render_template(self.template_name, **context)
class LotDeleteView(View):
methods = ['GET']
decorators = [login_required]
template_name = 'inventory/device_list.html'
def dispatch_request(self, id):
form = LotForm(id=id)
if form.instance.trade:
msg = "Sorry, the lot cannot be deleted because have a trade action "
messages.error(msg)
next_url = url_for('inventory.lotdevicelist', lot_id=id)
return flask.redirect(next_url)
form.remove()
next_url = url_for('inventory.devicelist')
return flask.redirect(next_url)
class UploadSnapshotView(GenericMixView):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/upload_snapshot.html'
def dispatch_request(self, lot_id=None):
lots = self.get_lots()
form = UploadSnapshotForm()
context = {
'page_title': 'Upload Snapshot',
'lots': lots,
'form': form,
'lot_id': lot_id,
'version': __version__,
}
if form.validate_on_submit():
snapshot = form.save(commit=False)
if lot_id:
lot = lots.filter(Lot.id == lot_id).one()
lot.devices.add(snapshot.device)
db.session.add(lot)
db.session.commit()
return flask.render_template(self.template_name, **context)
class DeviceCreateView(GenericMixView):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/device_create.html'
def dispatch_request(self, lot_id=None):
lots = self.get_lots()
form = NewDeviceForm()
context = {
'page_title': 'New Device',
'lots': lots,
'form': form,
'lot_id': lot_id,
'version': __version__,
}
if form.validate_on_submit():
snapshot = form.save(commit=False)
next_url = url_for('inventory.devicelist')
if lot_id:
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
lot = lots.filter(Lot.id == lot_id).one()
lot.devices.add(snapshot.device)
db.session.add(lot)
db.session.commit()
messages.success('Device "{}" created successfully!'.format(form.type.data))
return flask.redirect(next_url)
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):
methods = ['POST']
decorators = [login_required]
# template_name = 'inventory/device_list.html'
def dispatch_request(self):
form = TagDeviceForm()
if form.validate_on_submit():
form.save()
return flask.redirect(request.referrer)
class TagUnlinkDeviceView(View):
methods = ['POST', 'GET']
decorators = [login_required]
template_name = 'inventory/tag_unlink_device.html'
def dispatch_request(self, id):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
form = TagDeviceForm(delete=True, device=id)
if form.validate_on_submit():
form.remove()
next_url = url_for('inventory.devicelist')
return flask.redirect(next_url)
return flask.render_template(
self.template_name,
form=form,
lots=lots,
referrer=request.referrer,
version=__version__,
)
class NewActionView(View):
methods = ['POST']
decorators = [login_required]
form_class = NewActionForm
def dispatch_request(self):
self.form = self.form_class()
if self.form.validate_on_submit():
self.form.save()
messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url()
return flask.redirect(next_url)
def get_next_url(self):
lot_id = self.form.lot.data
if lot_id:
return url_for('inventory.lotdevicelist', lot_id=lot_id)
return url_for('inventory.devicelist')
class NewAllocateView(NewActionView, DeviceListMix):
methods = ['POST']
form_class = AllocateForm
def dispatch_request(self):
self.form = self.form_class()
if self.form.validate_on_submit():
self.form.save()
messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_allocate'] = self.form
return flask.render_template(self.template_name, **self.context)
class NewDataWipeView(NewActionView, DeviceListMix):
methods = ['POST']
form_class = DataWipeForm
def dispatch_request(self):
self.form = self.form_class()
if self.form.validate_on_submit():
self.form.save()
messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_datawipe'] = self.form
return flask.render_template(self.template_name, **self.context)
class NewTradeView(NewActionView, DeviceListMix):
methods = ['POST']
form_class = TradeForm
def dispatch_request(self):
self.form = self.form_class()
if self.form.validate_on_submit():
self.form.save()
messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_trade'] = self.form
return flask.render_template(self.template_name, **self.context)
class NewTradeDocumentView(View):
methods = ['POST', 'GET']
decorators = [login_required]
template_name = 'inventory/trade_document.html'
form_class = TradeDocumentForm
title = "Add new document"
def dispatch_request(self, lot_id):
self.form = self.form_class(lot=lot_id)
if self.form.validate_on_submit():
self.form.save()
messages.success('Document created successfully!')
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
return flask.redirect(next_url)
return flask.render_template(
self.template_name, form=self.form, title=self.title, version=__version__
)
class ExportsView(View):
methods = ['GET']
decorators = [login_required]
def dispatch_request(self, export_id):
export_ids = {
'metrics': self.metrics,
'devices': self.devices_list,
'certificates': self.erasure,
'links': self.public_links,
}
if export_id not in export_ids:
return NotFound()
return export_ids[export_id]()
def find_devices(self):
args = request.args.get('ids')
ids = args.split(',') if args else []
query = Device.query.filter(Device.owner == g.user)
return query.filter(Device.devicehub_id.in_(ids))
def response_csv(self, data, name):
bfile = data.getvalue().encode('utf-8')
# insert proof
insert_hash(bfile)
output = make_response(bfile)
output.headers['Content-Disposition'] = 'attachment; filename={}'.format(name)
output.headers['Content-type'] = 'text/csv'
return output
def devices_list(self):
"""Get device query and put information in csv format."""
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
first = True
for device in self.find_devices():
d = DeviceRow(device, {})
if first:
cw.writerow(d.keys())
first = False
cw.writerow(d.values())
return self.response_csv(data, "export.csv")
def metrics(self):
"""Get device query and put information in csv format."""
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
first = True
devs_id = []
# Get the allocate info
for device in self.find_devices():
devs_id.append(device.id)
for allocate in device.get_metrics():
d = ActionRow(allocate)
if first:
cw.writerow(d.keys())
first = False
cw.writerow(d.values())
# Get the trade info
query_trade = Trade.query.filter(
Trade.devices.any(Device.id.in_(devs_id))
).all()
lot_id = request.args.get('lot')
if lot_id and not query_trade:
lot = Lot.query.filter_by(id=lot_id).one()
if hasattr(lot, "trade") and lot.trade:
if g.user in [lot.trade.user_from, lot.trade.user_to]:
query_trade = [lot.trade]
for trade in query_trade:
data_rows = trade.get_metrics()
for row in data_rows:
d = ActionRow(row)
if first:
cw.writerow(d.keys())
first = False
cw.writerow(d.values())
return self.response_csv(data, "actions_export.csv")
def public_links(self):
# get a csv with the publink links of this devices
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
cw.writerow(['links'])
host_url = request.host_url
for dev in self.find_devices():
code = dev.devicehub_id
link = [f"{host_url}devices/{code}"]
cw.writerow(link)
return self.response_csv(data, "links.csv")
def erasure(self):
template = self.build_erasure_certificate()
res = flask_weasyprint.render_pdf(
flask_weasyprint.HTML(string=template),
download_filename='erasure-certificate.pdf',
)
insert_hash(res.data)
return res
def build_erasure_certificate(self):
erasures = []
for device in self.find_devices():
if isinstance(device, Computer):
for privacy in device.privacy:
erasures.append(privacy)
elif isinstance(device, DataStorage):
if device.privacy:
erasures.append(device.privacy)
params = {
'title': 'Erasure Certificate',
'erasures': tuple(erasures),
'url_pdf': '',
}
return flask.render_template('inventory/erasure.html', **params)
devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add'))
devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add'))
devices.add_url_rule(
'/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add')
)
devices.add_url_rule(
'/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add')
)
devices.add_url_rule(
'/lot/<string:lot_id>/trade-document/add/',
view_func=NewTradeDocumentView.as_view('trade_document_add'),
)
devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist'))
devices.add_url_rule(
'/device/<string:id>/', view_func=DeviceDetailView.as_view('device_details')
)
devices.add_url_rule(
'/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/<string:id>/del/', view_func=LotDeleteView.as_view('lot_del')
)
devices.add_url_rule('/lot/<string:id>/', view_func=LotUpdateView.as_view('lot_edit'))
devices.add_url_rule(
'/upload-snapshot/', view_func=UploadSnapshotView.as_view('upload_snapshot')
)
devices.add_url_rule(
'/lot/<string:lot_id>/upload-snapshot/',
view_func=UploadSnapshotView.as_view('lot_upload_snapshot'),
)
devices.add_url_rule('/device/add/', view_func=DeviceCreateView.as_view('device_add'))
devices.add_url_rule(
'/lot/<string:lot_id>/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(
'/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add')
)
devices.add_url_rule(
'/tag/devices/<int:id>/del/',
view_func=TagUnlinkDeviceView.as_view('tag_devices_del'),
)
devices.add_url_rule(
'/export/<string:export_id>/', view_func=ExportsView.as_view('export')
)

View File

@ -0,0 +1,64 @@
from flask import flash, session
DEBUG = 10
INFO = 20
SUCCESS = 25
WARNING = 30
ERROR = 40
DEFAULT_LEVELS = {
'DEBUG': DEBUG,
'INFO': INFO,
'SUCCESS': SUCCESS,
'WARNING': WARNING,
'ERROR': ERROR,
}
DEFAULT_TAGS = {
DEBUG: 'light',
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'danger',
}
DEFAULT_ICONS = {
DEFAULT_TAGS[DEBUG]: 'tools',
DEFAULT_TAGS[INFO]: 'info-circle',
DEFAULT_TAGS[SUCCESS]: 'check-circle',
DEFAULT_TAGS[WARNING]: 'exclamation-triangle',
DEFAULT_TAGS[ERROR]: 'exclamation-octagon',
}
def add_message(level, message):
level_tag = DEFAULT_TAGS[level]
if '_message_icon' not in session:
session['_message_icon'] = DEFAULT_ICONS
flash(message, level_tag)
def debug(message):
"""Add a message with the ``DEBUG`` level."""
add_message(DEBUG, message)
def info(message):
"""Add a message with the ``INFO`` level."""
add_message(INFO, message)
def success(message):
"""Add a message with the ``SUCCESS`` level."""
add_message(SUCCESS, message)
def warning(message):
"""Add a message with the ``WARNING`` level."""
add_message(WARNING, message)
def error(message):
"""Add a message with the ``ERROR`` level."""
add_message(ERROR, message)

View File

@ -213,18 +213,6 @@ class Action(Thing):
args[INHERIT_COND] = cls.id == Action.id
return args
@validates('end_time')
def validate_end_time(self, _, end_time: datetime):
if self.start_time and end_time <= self.start_time:
raise ValidationError('The action cannot finish before it starts.')
return end_time
@validates('start_time')
def validate_start_time(self, _, start_time: datetime):
if self.end_time and start_time >= self.end_time:
raise ValidationError('The action cannot start after it finished.')
return start_time
@property
def date_str(self):
return '{:%c}'.format(self.end_time)

View File

@ -55,6 +55,10 @@ class Action(Thing):
if 'start_time' in data and data['start_time'] < unix_time:
data['start_time'] = unix_time
if data.get('end_time') and data.get('start_time'):
if data['start_time'] > data['end_time']:
raise ValidationError('The action cannot finish before it starts.')
class ActionWithOneDevice(Action):
__doc__ = m.ActionWithOneDevice.__doc__

View File

@ -164,6 +164,10 @@ class Device(Thing):
super().__init__(**kw)
self.set_hid()
@property
def reverse_actions(self) -> list:
return reversed(self.actions)
@property
def actions(self) -> list:
"""All the actions where the device participated, including:
@ -470,6 +474,13 @@ class Device(Thing):
key=attrgetter('type')) # last test of each type
return self._warning_actions(current_tests)
@property
def verbose_name(self):
type = self.type or ''
manufacturer = self.manufacturer or ''
model = self.model or ''
return f'{type} {manufacturer} {model}'
@declared_attr
def __mapper_args__(cls):
"""Defines inheritance.

View File

@ -26,13 +26,16 @@ class ReportHash(db.Model):
hash3.comment = """The normalized name of the hash."""
def insert_hash(bfile):
def insert_hash(bfile, commit=True):
hash3 = hashlib.sha3_256(bfile).hexdigest()
db_hash = ReportHash(hash3=hash3)
db.session.add(db_hash)
if commit:
db.session.commit()
db.session.flush()
return hash3
def verify_hash(bfile):
hash3 = hashlib.sha3_256(bfile.read()).hexdigest()

View File

@ -5,6 +5,7 @@ from typing import Union
from boltons import urlutils
from citext import CIText
from flask import g
from flask_login import current_user
from sqlalchemy import TEXT
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy_utils import LtreeType
@ -101,7 +102,15 @@ class Lot(Thing):
@property
def is_temporary(self):
return False if self.trade else True
return not bool(self.trade)
@property
def is_incoming(self):
return bool(self.trade and self.trade.user_to == current_user)
@property
def is_outgoing(self):
return bool(self.trade and self.trade.user_from == current_user)
@classmethod
def descendantsq(cls, id):

View File

@ -136,6 +136,10 @@ class Tag(Thing):
def code(self) -> str:
return hashcode.encode(self.internal_id)
@property
def get_provider(self) -> str:
return self.provider.to_text() if self.provider else ''
def delete(self):
"""Deletes the tag.

View File

@ -1,7 +1,7 @@
from uuid import uuid4
from citext import CIText
from flask import current_app as app
from flask_login import UserMixin
from sqlalchemy import Column, Boolean, BigInteger, Sequence
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy_utils import EmailType, PasswordType
@ -13,7 +13,7 @@ from ereuse_devicehub.resources.models import STR_SIZE, Thing
from ereuse_devicehub.resources.enums import SessionType
class User(Thing):
class User(UserMixin, Thing):
__table_args__ = {'schema': 'common'}
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
email = Column(EmailType, nullable=False, unique=True)
@ -66,6 +66,21 @@ class User(Thing):
return
return self.email.split('@')[0].split('_')[1]
@property
def is_active(self):
"""Alias because flask-login expects `is_active` attribute"""
return self.active
@property
def get_full_name(self):
# TODO(@slamora) create first_name & last_name fields and use
# them to generate user full name
return self.email
def check_password(self, password):
# take advantage of SQL Alchemy PasswordType to verify password
return self.password == password
class UserInventory(db.Model):
"""Relationship between users and their inventories."""

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 59.641495 19.325608"
height="19.325607mm"
width="59.641495mm">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-149.20084,-143.42372)"
id="layer1">
<text
id="text823"
y="158.73648"
x="80.130806"
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve"><tspan
style="font-size:22.57777786px;stroke-width:0.26458332"
id="tspan821"
y="158.73648"
x="147.32671"><tspan
id="tspan819"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.57777786px;font-family:Calibri;-inkscape-font-specification:Calibri;fill:#993366;fill-opacity:1;stroke-width:0.26458332"
y="158.73648"
x="147.32671"><tspan
id="tspan825"
style="fill:#0a0406;fill-opacity:1">USO</tspan>dy</tspan></tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

File diff suppressed because one or more lines are too long

164
ereuse_devicehub/static/js/jspdf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,220 @@
/**
* Template Name: NiceAdmin - v2.2.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
*/
(function() {
"use strict";
/**
* Easy selector helper function
*/
const select = (el, all = false) => {
el = el.trim()
if (all) {
return [...document.querySelectorAll(el)]
} else {
return document.querySelector(el)
}
}
/**
* Easy event listener function
*/
const on = (type, el, listener, all = false) => {
if (all) {
select(el, all).forEach(e => e.addEventListener(type, listener))
} else {
select(el, all).addEventListener(type, listener)
}
}
/**
* Easy on scroll event listener
*/
const onscroll = (el, listener) => {
el.addEventListener('scroll', listener)
}
/**
* Sidebar toggle
*/
if (select('.toggle-sidebar-btn')) {
on('click', '.toggle-sidebar-btn', function(e) {
select('body').classList.toggle('toggle-sidebar')
})
}
/**
* Search bar toggle
*/
if (select('.search-bar-toggle')) {
on('click', '.search-bar-toggle', function(e) {
select('.search-bar').classList.toggle('search-bar-show')
})
}
/**
* Navbar links active state on scroll
*/
let navbarlinks = select('#navbar .scrollto', true)
const navbarlinksActive = () => {
let position = window.scrollY + 200
navbarlinks.forEach(navbarlink => {
if (!navbarlink.hash) return
let section = select(navbarlink.hash)
if (!section) return
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
navbarlink.classList.add('active')
} else {
navbarlink.classList.remove('active')
}
})
}
window.addEventListener('load', navbarlinksActive)
onscroll(document, navbarlinksActive)
/**
* Toggle .header-scrolled class to #header when page is scrolled
*/
let selectHeader = select('#header')
if (selectHeader) {
const headerScrolled = () => {
if (window.scrollY > 100) {
selectHeader.classList.add('header-scrolled')
} else {
selectHeader.classList.remove('header-scrolled')
}
}
window.addEventListener('load', headerScrolled)
onscroll(document, headerScrolled)
}
/**
* Back to top button
*/
let backtotop = select('.back-to-top')
if (backtotop) {
const toggleBacktotop = () => {
if (window.scrollY > 100) {
backtotop.classList.add('active')
} else {
backtotop.classList.remove('active')
}
}
window.addEventListener('load', toggleBacktotop)
onscroll(document, toggleBacktotop)
}
/**
* Initiate tooltips
*/
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function(tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
/**
* Initiate quill editors
*/
if (select('.quill-editor-default')) {
new Quill('.quill-editor-default', {
theme: 'snow'
});
}
if (select('.quill-editor-bubble')) {
new Quill('.quill-editor-bubble', {
theme: 'bubble'
});
}
if (select('.quill-editor-full')) {
new Quill(".quill-editor-full", {
modules: {
toolbar: [
[{
font: []
}, {
size: []
}],
["bold", "italic", "underline", "strike"],
[{
color: []
},
{
background: []
}
],
[{
script: "super"
},
{
script: "sub"
}
],
[{
list: "ordered"
},
{
list: "bullet"
},
{
indent: "-1"
},
{
indent: "+1"
}
],
["direction", {
align: []
}],
["link", "image", "video"],
["clean"]
]
},
theme: "snow"
});
}
/**
* Initiate Bootstrap validation check
*/
var needsValidation = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(needsValidation)
.forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
/**
* Initiate Datatables
*/
const datatables = select('.datatable', true)
datatables.forEach(datatable => {
new simpleDatatables.DataTable(datatable);
})
/**
* Autoresize echart charts
*/
const mainContainer = select('#main');
if (mainContainer) {
setTimeout(() => {
new ResizeObserver(function() {
select('.echart', true).forEach(getEchart => {
echarts.getInstanceByDom(getEchart).resize();
})
}).observe(mainContainer);
}, 200);
}
})();

View File

@ -0,0 +1,182 @@
$(document).ready(function() {
var show_allocate_form = $("#allocateModal").data('show-action-form');
var show_datawipe_form = $("#datawipeModal").data('show-action-form');
var show_trade_form = $("#tradeLotModal").data('show-action-form');
if (show_allocate_form != "None") {
$("#allocateModal .btn-primary").show();
newAllocate(show_allocate_form);
} else if (show_datawipe_form != "None") {
$("#datawipeModal .btn-primary").show();
newDataWipe(show_datawipe_form);
} else if (show_trade_form != "None") {
$("#tradeLotModal .btn-primary").show();
newTrade(show_trade_form);
} else {
$(".deviceSelect").on("change", deviceSelect);
}
// $('#selectLot').selectpicker();
})
function deviceSelect() {
var devices_count = $(".deviceSelect").filter(':checked').length;
get_device_list();
if (devices_count == 0) {
$("#addingLotModal .pol").show();
$("#addingLotModal .btn-primary").hide();
$("#removeLotModal .pol").show();
$("#removeLotModal .btn-primary").hide();
$("#addingTagModal .pol").show();
$("#addingTagModal .btn-primary").hide();
$("#actionModal .pol").show();
$("#actionModal .btn-primary").hide();
$("#allocateModal .pol").show();
$("#allocateModal .btn-primary").hide();
$("#datawipeModal .pol").show();
$("#datawipeModal .btn-primary").hide();
} else {
$("#addingLotModal .pol").hide();
$("#addingLotModal .btn-primary").show();
$("#removeLotModal .pol").hide();
$("#removeLotModal .btn-primary").show();
$("#actionModal .pol").hide();
$("#actionModal .btn-primary").show();
$("#allocateModal .pol").hide();
$("#allocateModal .btn-primary").show();
$("#datawipeModal .pol").hide();
$("#datawipeModal .btn-primary").show();
$("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show();
}
}
function removeLot() {
var devices = $(".deviceSelect");
if (devices.length > 0) {
$("#btnRemoveLots .text-danger").show();
} else {
$("#btnRemoveLots .text-danger").hide();
}
$("#activeRemoveLotModal").click();
}
function removeTag() {
var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
if (devices_id.length == 1) {
var url = "/inventory/tag/devices/"+devices_id[0]+"/del/";
window.location.href = url;
} else {
$("#unlinkTagAlertModal").click();
}
}
function addTag() {
var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
if (devices_id.length == 1) {
$("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show();
} else {
$("#addingTagModal .pol").show();
$("#addingTagModal .btn-primary").hide();
}
$("#addTagAlertModal").click();
}
function newTrade(action) {
var title = "Trade "
var user_to = $("#user_to").data("email");
var user_from = $("#user_from").data("email");
if (action == 'user_from') {
title = 'Trade Incoming';
$("#user_to").attr('readonly', 'readonly');
$("#user_from").prop('readonly', false);
$("#user_from").val('');
$("#user_to").val(user_to);
} else if (action == 'user_to') {
title = 'Trade Outgoing';
$("#user_from").attr('readonly', 'readonly');
$("#user_to").prop('readonly', false);
$("#user_to").val('');
$("#user_from").val(user_from);
}
$("#tradeLotModal #title-action").html(title);
$("#activeTradeModal").click();
}
function newAction(action) {
$("#actionModal #type").val(action);
$("#actionModal #title-action").html(action);
deviceSelect();
$("#activeActionModal").click();
}
function newAllocate(action) {
$("#allocateModal #type").val(action);
$("#allocateModal #title-action").html(action);
deviceSelect();
$("#activeAllocateModal").click();
}
function newDataWipe(action) {
$("#datawipeModal #type").val(action);
$("#datawipeModal #title-action").html(action);
deviceSelect();
$("#activeDatawipeModal").click();
}
function get_device_list() {
var devices = $(".deviceSelect").filter(':checked');
/* Insert the correct count of devices in actions form */
var devices_count = devices.length;
$("#datawipeModal .devices-count").html(devices_count);
$("#allocateModal .devices-count").html(devices_count);
$("#actionModal .devices-count").html(devices_count);
/* Insert the correct value in the input devicesList */
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(",");
$.map($(".devicesList"), function(x) {
$(x).val(devices_id);
});
/* Create a list of devices for human representation */
var computer = {
"Desktop": "<i class='bi bi-building'></i>",
"Laptop": "<i class='bi bi-laptop'></i>",
};
list_devices = devices.map(function (x) {
var typ = $(devices[x]).data("device-type");
var manuf = $(devices[x]).data("device-manufacturer");
var dhid = $(devices[x]).data("device-dhid");
if (computer[typ]) {
typ = computer[typ];
};
return typ + " " + manuf + " " + dhid;
});
description = $.map(list_devices, function(x) { return x }).join(", ");
$(".enumeration-devices").html(description);
}
function export_file(type_file) {
var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(",");
if (devices_id){
var url = "/inventory/export/"+type_file+"/?ids="+devices_id;
window.location.href = url;
} else {
$("#exportAlertModal").click();
}
}

View File

@ -0,0 +1,69 @@
$(document).ready(function() {
STORAGE_KEY = 'tag-spec-key';
$("#printerType").on("change", change_size);
change_size();
load_size();
})
function qr_draw(url) {
var qrcode = new QRCode($("#qrcode")[0], {
text: url,
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.Q
});
}
function save_size() {
var height = $("#height-tag").val();
var width = $("#width-tag").val();
var sizePreset = $("#printerType").val();
var data = {"height": height, "width": width, "sizePreset": sizePreset};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function load_size() {
var data = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (data){
$("#height-tag").val(data.height);
$("#width-tag").val(data.width);
$("#printerType").val(data.sizePreset);
};
}
function reset_size() {
localStorage.removeItem(STORAGE_KEY);
$("#printerType").val('brotherSmall');
change_size();
}
function change_size() {
var sizePreset = $("#printerType").val();
if (sizePreset == 'brotherSmall') {
$("#height-tag").val(29);
$("#width-tag").val(62);
} else if (sizePreset == 'smallTagPrinter') {
$("#height-tag").val(59);
$("#width-tag").val(97);
}
}
function printpdf() {
var border = 2;
var height = parseInt($("#height-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;
max_tag_side = (Math.max(height, width)/2) + border;
if (max_tag_side < img_side) {
max_tag_side = img_side+ 2*border;
};
min_tag_side = (Math.min(height, width)/2) + border;
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side);
pdf.text(tag, max_tag_side, min_tag_side);
pdf.save('Tag_'+tag+'.pdf');
}

View File

@ -0,0 +1,614 @@
/**
* @fileoverview
* - Using the 'QRCode for Javascript library'
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
* - this library has no dependencies.
*
* @author davidshimjs
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
var QRCode;
(function () {
//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
// http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE;
this.data = data;
this.parsedData = [];
// Added to support UTF-8 Characters
for (var i = 0, l = this.data.length; i < l; i++) {
var byteArray = [];
var code = this.data.charCodeAt(i);
if (code > 0x10000) {
byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[3] = 0x80 | (code & 0x3F);
} else if (code > 0x800) {
byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[2] = 0x80 | (code & 0x3F);
} else if (code > 0x80) {
byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
byteArray[1] = 0x80 | (code & 0x3F);
} else {
byteArray[0] = code;
}
this.parsedData.push(byteArray);
}
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
if (this.parsedData.length != this.data.length) {
this.parsedData.unshift(191);
this.parsedData.unshift(187);
this.parsedData.unshift(239);
}
}
QR8bitByte.prototype = {
getLength: function (buffer) {
return this.parsedData.length;
},
write: function (buffer) {
for (var i = 0, l = this.parsedData.length; i < l; i++) {
buffer.put(this.parsedData[i], 8);
}
}
};
function QRCodeModel(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
+buffer.getLengthInBits()
+">"
+totalDataCount*8
+")");}
if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD1,8);}
return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=256){n-=255;}
return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
function _isSupportCanvas() {
return typeof CanvasRenderingContext2D != "undefined";
}
// android 2.x doesn't support Data-URI spec
function _getAndroid() {
var android = false;
var sAgent = navigator.userAgent;
if (/android/i.test(sAgent)) { // android
android = true;
var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
if (aMat && aMat[1]) {
android = parseFloat(aMat[1]);
}
}
return android;
}
var svgDrawer = (function() {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
this.clear();
function makeSVG(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
return el;
}
var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
_el.appendChild(svg);
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
if (oQRCode.isDark(row, col)) {
var child = makeSVG("use", {"x": String(col), "y": String(row)});
child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
svg.appendChild(child);
}
}
}
};
Drawing.prototype.clear = function () {
while (this._el.hasChildNodes())
this._el.removeChild(this._el.lastChild);
};
return Drawing;
})();
var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
// Drawing in DOM by using Table tag
var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_el.innerHTML = aHTML.join('');
// Fix the margin values as real size.
var elTable = _el.childNodes[0];
var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";
}
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._el.innerHTML = '';
};
return Drawing;
})() : (function () { // Drawing in Canvas
function _onMakeImage() {
this._elImage.src = this._elCanvas.toDataURL("image/png");
this._elImage.style.display = "block";
this._elCanvas.style.display = "none";
}
// Android 2.1 bug workaround
// http://code.google.com/p/android/issues/detail?id=5141
if (this._android && this._android <= 2.1) {
var factor = 1 / window.devicePixelRatio;
var drawImage = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (("nodeName" in image) && /img/i.test(image.nodeName)) {
for (var i = arguments.length - 1; i >= 1; i--) {
arguments[i] = arguments[i] * factor;
}
} else if (typeof dw == "undefined") {
arguments[1] *= factor;
arguments[2] *= factor;
arguments[3] *= factor;
arguments[4] *= factor;
}
drawImage.apply(this, arguments);
};
}
/**
* Check whether the user's browser supports Data URI or not
*
* @private
* @param {Function} fSuccess Occurs if it supports Data URI
* @param {Function} fFail Occurs if it doesn't support Data URI
*/
function _safeSetDataURI(fSuccess, fFail) {
var self = this;
self._fFail = fFail;
self._fSuccess = fSuccess;
// Check it just once
if (self._bSupportDataURI === null) {
var el = document.createElement("img");
var fOnError = function() {
self._bSupportDataURI = false;
if (self._fFail) {
self._fFail.call(self);
}
};
var fOnSuccess = function() {
self._bSupportDataURI = true;
if (self._fSuccess) {
self._fSuccess.call(self);
}
};
el.onabort = fOnError;
el.onerror = fOnError;
el.onload = fOnSuccess;
el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
return;
} else if (self._bSupportDataURI === true && self._fSuccess) {
self._fSuccess.call(self);
} else if (self._bSupportDataURI === false && self._fFail) {
self._fFail.call(self);
}
};
/**
* Drawing QRCode by using canvas
*
* @constructor
* @param {HTMLElement} el
* @param {Object} htOption QRCode Options
*/
var Drawing = function (el, htOption) {
this._bIsPainted = false;
this._android = _getAndroid();
this._htOption = htOption;
this._elCanvas = document.createElement("canvas");
this._elCanvas.width = htOption.width;
this._elCanvas.height = htOption.height;
el.appendChild(this._elCanvas);
this._el = el;
this._oContext = this._elCanvas.getContext("2d");
this._bIsPainted = false;
this._elImage = document.createElement("img");
this._elImage.alt = "Scan me!";
this._elImage.style.display = "none";
this._el.appendChild(this._elImage);
this._bSupportDataURI = null;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _elImage = this._elImage;
var _oContext = this._oContext;
var _htOption = this._htOption;
var nCount = oQRCode.getModuleCount();
var nWidth = _htOption.width / nCount;
var nHeight = _htOption.height / nCount;
var nRoundedWidth = Math.round(nWidth);
var nRoundedHeight = Math.round(nHeight);
_elImage.style.display = "none";
this.clear();
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
var bIsDark = oQRCode.isDark(row, col);
var nLeft = col * nWidth;
var nTop = row * nHeight;
_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.lineWidth = 1;
_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
// 안티 앨리어싱 방지 처리
_oContext.strokeRect(
Math.floor(nLeft) + 0.5,
Math.floor(nTop) + 0.5,
nRoundedWidth,
nRoundedHeight
);
_oContext.strokeRect(
Math.ceil(nLeft) - 0.5,
Math.ceil(nTop) - 0.5,
nRoundedWidth,
nRoundedHeight
);
}
}
this._bIsPainted = true;
};
/**
* Make the image from Canvas if the browser supports Data URI.
*/
Drawing.prototype.makeImage = function () {
if (this._bIsPainted) {
_safeSetDataURI.call(this, _onMakeImage);
}
};
/**
* Return whether the QRCode is painted or not
*
* @return {Boolean}
*/
Drawing.prototype.isPainted = function () {
return this._bIsPainted;
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
this._bIsPainted = false;
};
/**
* @private
* @param {Number} nNumber
*/
Drawing.prototype.round = function (nNumber) {
if (!nNumber) {
return nNumber;
}
return Math.floor(nNumber * 1000) / 1000;
};
return Drawing;
})();
/**
* Get the type by string length
*
* @private
* @param {String} sText
* @param {Number} nCorrectLevel
* @return {Number} type
*/
function _getTypeNumber(sText, nCorrectLevel) {
var nType = 1;
var length = _getUTF8Length(sText);
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
var nLimit = 0;
switch (nCorrectLevel) {
case QRErrorCorrectLevel.L :
nLimit = QRCodeLimitLength[i][0];
break;
case QRErrorCorrectLevel.M :
nLimit = QRCodeLimitLength[i][1];
break;
case QRErrorCorrectLevel.Q :
nLimit = QRCodeLimitLength[i][2];
break;
case QRErrorCorrectLevel.H :
nLimit = QRCodeLimitLength[i][3];
break;
}
if (length <= nLimit) {
break;
} else {
nType++;
}
}
if (nType > QRCodeLimitLength.length) {
throw new Error("Too long data");
}
return nType;
}
function _getUTF8Length(sText) {
var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
return replacedText.length + (replacedText.length != sText ? 3 : 0);
}
/**
* @class QRCode
* @constructor
* @example
* new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
*
* @example
* var oQRCode = new QRCode("test", {
* text : "http://naver.com",
* width : 128,
* height : 128
* });
*
* oQRCode.clear(); // Clear the QRCode.
* oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
*
* @param {HTMLElement|String} el target element or 'id' attribute of element.
* @param {Object|String} vOption
* @param {String} vOption.text QRCode link data
* @param {Number} [vOption.width=256]
* @param {Number} [vOption.height=256]
* @param {String} [vOption.colorDark="#000000"]
* @param {String} [vOption.colorLight="#ffffff"]
* @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
*/
QRCode = function (el, vOption) {
this._htOption = {
width : 256,
height : 256,
typeNumber : 4,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRErrorCorrectLevel.H
};
if (typeof vOption === 'string') {
vOption = {
text : vOption
};
}
// Overwrites options
if (vOption) {
for (var i in vOption) {
this._htOption[i] = vOption[i];
}
}
if (typeof el == "string") {
el = document.getElementById(el);
}
if (this._htOption.useSVG) {
Drawing = svgDrawer;
}
this._android = _getAndroid();
this._el = el;
this._oQRCode = null;
this._oDrawing = new Drawing(this._el, this._htOption);
if (this._htOption.text) {
this.makeCode(this._htOption.text);
}
};
/**
* Make the QRCode
*
* @param {String} sText link data
*/
QRCode.prototype.makeCode = function (sText) {
this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
this._oQRCode.addData(sText);
this._oQRCode.make();
this._el.title = sText;
this._oDrawing.draw(this._oQRCode);
this.makeImage();
};
/**
* Make the Image from Canvas element
* - It occurs automatically
* - Android below 3 doesn't support Data-URI spec.
*
* @private
*/
QRCode.prototype.makeImage = function () {
if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
this._oDrawing.makeImage();
}
};
/**
* Clear the QRCode
*/
QRCode.prototype.clear = function () {
this._oDrawing.clear();
};
/**
* @name QRCode.CorrectLevel
*/
QRCode.CorrectLevel = QRErrorCorrectLevel;
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,485 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,482 @@
/*!
* Bootstrap Reboot v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>{% block page_title %}{% endblock %} - USOdy</title>
<meta content="" name="description">
<meta content="" name="keywords">
<!-- Favicons -->
<link href="{{ url_for('static', filename='img/favicon.png') }}" rel="icon">
<link href="{{ url_for('static', filename='img/apple-touch-icon.png') }}" rel="apple-touch-icon">
<!-- Google Fonts -->
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
<!-- JS Files -->
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script>
<!-- Vendor CSS Files -->
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/simple-datatables@latest/dist/style.css" rel="stylesheet" type="text/css">
<!-- Template Main CSS File -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<!-- =======================================================
* Template Name: NiceAdmin - v2.2.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
======================================================== -->
</head>
<body>
{% block body %}{% endblock %}
<a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
<!-- Vendor JS Files -->
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
<!-- Template Main JS File -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

View File

@ -0,0 +1,223 @@
{% extends "ereuse_devicehub/base.html" %}
{% block page_title %}{{ page_title }}{% endblock %}
{% block body %}
<!-- ======= Header ======= -->
<header id="header" class="header fixed-top d-flex align-items-center">
<div class="d-flex align-items-center justify-content-between">
<a href="{{ url_for('inventory.devicelist')}}" class="logo d-flex align-items-center">
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
</a>
<i class="bi bi-list toggle-sidebar-btn"></i>
</div><!-- End Logo -->
<div class="search-bar">
<form class="search-form d-flex align-items-center" method="POST" action="#">
<input type="text" name="query" placeholder="Search" title="Enter search keyword">
<button type="submit" title="Search"><i class="bi bi-search"></i></button>
</form>
</div><!-- End Search Bar -->
<nav class="header-nav ms-auto">
<ul class="d-flex align-items-center">
<li class="nav-item d-block d-lg-none">
<a class="nav-link nav-icon search-bar-toggle " href="#">
<i class="bi bi-search"></i>
</a>
</li><!-- End Search Icon-->
<li class="nav-item dropdown pe-3">
<a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown">
<i class="bi bi-person-circle" style="font-size: 36px;"></i>
<span class="d-none d-md-block dropdown-toggle ps-2">{{ current_user.email }}</span>
</a><!-- End Profile Iamge Icon -->
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
<li class="dropdown-header">
<h6>{{ current_user.get_full_name }}</h6>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item d-flex align-items-center" href="{{ url_for('core.user-profile') }}">
<i class="bi bi-person"></i>
<span>My Profile</span>
</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item d-flex align-items-center" href="pages-faq.html">
<i class="bi bi-question-circle"></i>
<span>Need Help?</span>
</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item d-flex align-items-center" href="{{ url_for('core.logout') }}">
<i class="bi bi-box-arrow-right"></i>
<span>Sign Out</span>
</a>
</li>
</ul><!-- End Profile Dropdown Items -->
</li><!-- End Profile Nav -->
</ul>
</nav><!-- End Icons Navigation -->
</header><!-- End Header -->
<!-- ======= Sidebar ======= -->
<aside id="sidebar" class="sidebar">
<ul class="sidebar-nav" id="sidebar-nav">
<!-- We need defined before the Dashboard
<li class="nav-item">
<a class="nav-link collapsed" href="index.html">
<i class="bi bi-grid"></i>
<span>Dashboard</span>
</a>
</li><!-- End Dashboard Nav -->
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.devicelist') }}">
<i class="bi-menu-button-wide"></i>
<span>Unassigned devices</span>
</a>
</li>
<li class="nav-heading">Lots</li>
<li class="nav-item">
{% if lot and lot.is_incoming %}
<a class="nav-link" data-bs-target="#incoming-lots-nav" data-bs-toggle="collapse" href="#">
{% else %}
<a class="nav-link collapsed" data-bs-target="#incoming-lots-nav" data-bs-toggle="collapse" href="#">
{% endif %}
<i class="bi bi-arrow-down-right"></i><span>Incoming Lots</span><i class="bi bi-chevron-down ms-auto"></i>
</a>
{% if lot and lot.is_incoming %}
<ul id="incoming-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
{% else %}
<ul id="incoming-lots-nav" class="nav-content collapse" data-bs-parent="#sidebar-nav">
{% endif %}
{% for lot in lots %}
{% if lot.is_incoming %}
<li>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<i class="bi bi-circle"></i><span>{{ lot.name }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</li><!-- End Incoming Lots Nav -->
<li class="nav-item">
{% if lot and lot.is_outgoing %}
<a class="nav-link" data-bs-target="#outgoing-lots-nav" data-bs-toggle="collapse" href="#">
{% else %}
<a class="nav-link collapsed" data-bs-target="#outgoing-lots-nav" data-bs-toggle="collapse" href="#">
{% endif %}
<i class="bi bi-arrow-up-right"></i><span>Outgoing Lots</span><i class="bi bi-chevron-down ms-auto"></i>
</a>
{% if lot and lot.is_outgoing %}
<ul id="outgoing-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
{% else %}
<ul id="outgoing-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
{% endif %}
{% for lot in lots %}
{% if lot.is_outgoing %}
<li>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<i class="bi bi-circle"></i><span>{{ lot.name }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</li><!-- End Outgoing Lots Nav -->
<li class="nav-item">
{% if lot and lot.is_temporary %}
<a class="nav-link" data-bs-target="#temporal-lots-nav" data-bs-toggle="collapse" href="#">
{% else %}
<a class="nav-link collapsed" data-bs-target="#temporal-lots-nav" data-bs-toggle="collapse" href="#">
{% endif %}
<i class="bi bi-layout-text-window-reverse"></i><span>Temporary Lots</span><i class="bi bi-chevron-down ms-auto"></i>
</a>
{% if lot and lot.is_temporary %}
<ul id="temporal-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
{% else %}
<ul id="temporal-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
{% endif %}
<li>
<a href="{{ url_for('inventory.lot_add')}}">
<i class="bi bi-plus" style="font-size: larger;"></i><span>New temporary lot</span>
</a>
</li>
{% for lot in lots %}
{% if lot.is_temporary %}
<li>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
<i class="bi bi-circle"></i><span>{{ lot.name }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</li><!-- End Temporal Lots Nav -->
<li class="nav-heading">Utils</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.taglist')}}">
<i class="bi bi-tags"></i>
<span>Tags</span>
</a>
</li><!-- End Tags Page Nav -->
</ul>
</aside><!-- End Sidebar-->
<main id="main" class="main">
{% block messages %}
{% for level, message in get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{ level}} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endblock %}
{% block main %}
{% endblock main %}
</main><!-- End #main -->
<!-- ======= Footer ======= -->
<footer id="footer" class="footer">
<div class="copyright">
&copy; Copyright <strong><span>USOdy</span></strong>. All Rights Reserved
</div>
<div class="credits">
<!-- All the links in the footer should remain intact. -->
<!-- You can delete the links only if you purchased the pro version. -->
<!-- Licensing information: https://bootstrapmade.com/license/ -->
<!-- Purchase the pro version with working PHP/AJAX contact form: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ -->
Designed by <a href="https://bootstrapmade.com/">BootstrapMade</a> // DeviceHub {{ version }}
</div>
</footer><!-- End Footer -->
{% endblock body %}

View File

@ -0,0 +1,86 @@
{% extends "ereuse_devicehub/base.html" %}
{% block page_title %}Login{% endblock %}
{% block body %}
<main>
<div class="container">
<section class="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">
<div class="d-flex justify-content-center py-4">
<a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
</a>
</div><!-- End Logo -->
<div class="card mb-3">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Login to Your Account</h5>
<p class="text-center small">Enter your username & password to login</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<form method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
<div class="col-12">
<label for="yourEmail" class="form-label">Email</label>
<input type="email" name="email" class="form-control" id="yourEmail" required value="{{ form.email.data|default('', true) }}">
<div class="invalid-feedback">Please enter your email.</div>
</div>
<div class="col-12">
<label for="yourPassword" class="form-label">Password</label>
<input type="password" name="password" class="form-control" id="yourPassword" required>
<div class="invalid-feedback">Please enter your password!</div>
</div>
<!-- TODO(@slamora): hidde until it is implemented
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" {% if form.remember.data %}checked{% endif %} id="rememberMe">
<label class="form-check-label" for="rememberMe">Remember me</label>
</div>
</div>
-->
<div class="col-12">
<button class="btn btn-primary w-100" type="submit">Login</button>
</div>
<div class="col-12">
<p class="small mb-0">Don't have account? <a href="#TODO">Create an account</a></p>
</div>
</form>
</div>
</div>
<div class="credits">
<!-- All the links in the footer should remain intact. -->
<!-- You can delete the links only if you purchased the pro version. -->
<!-- Licensing information: https://bootstrapmade.com/license/ -->
<!-- Purchase the pro version with working PHP/AJAX contact form: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ -->
Designed by <a href="https://bootstrapmade.com/">BootstrapMade</a>
</div>
</div>
</div>
</div>
</section>
</div>
</main><!-- End #main -->
{% endblock body %}

View File

@ -0,0 +1,288 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block page_title %}Your Profile{% endblock %}
{% block main %}
<div class="pagetitle">
<h1>Profile</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
<li class="breadcrumb-item">Users</li>
<li class="breadcrumb-item active">Profile</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-4">
<div class="card">
<div class="card-body profile-card pt-4 d-flex flex-column align-items-center">
<i class="bi bi-person-circle" style="font-size: 76px;"></i>
<h2>{{ current_user.get_full_name }}</h2>
</div>
</div>
</div>
<div class="col-xl-8">
<div class="card">
<div class="card-body pt-3">
<!-- Bordered Tabs -->
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#profile-overview">Overview</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-edit">Edit Profile</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-settings">Settings</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-change-password">Change Password</button>
</li>
</ul>
<div class="tab-content pt-2">
<div class="tab-pane fade show active profile-overview" id="profile-overview">
<h5 class="card-title">About</h5>
<p class="small fst-italic">Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.</p>
<h5 class="card-title">Profile Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Full Name</div>
<div class="col-lg-9 col-md-8">Kevin Anderson</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Company</div>
<div class="col-lg-9 col-md-8">Lueilwitz, Wisoky and Leuschke</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Job</div>
<div class="col-lg-9 col-md-8">Web Designer</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Country</div>
<div class="col-lg-9 col-md-8">USA</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Address</div>
<div class="col-lg-9 col-md-8">A108 Adam Street, New York, NY 535022</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Phone</div>
<div class="col-lg-9 col-md-8">(436) 486-3538 x29071</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Email</div>
<div class="col-lg-9 col-md-8">k.anderson@example.com</div>
</div>
</div>
<div class="tab-pane fade profile-edit pt-3" id="profile-edit">
<!-- Profile Edit Form -->
<form>
<div class="row mb-3">
<label for="profileImage" class="col-md-4 col-lg-3 col-form-label">Profile Image</label>
<div class="col-md-8 col-lg-9">
<img src="{{ url_for('static', filename='img/profile-img.jpg') }}" alt="Profile">
<div class="pt-2">
<a href="#" class="btn btn-primary btn-sm" title="Upload new profile image"><i class="bi bi-upload"></i></a>
<a href="#" class="btn btn-danger btn-sm" title="Remove my profile image"><i class="bi bi-trash"></i></a>
</div>
</div>
</div>
<div class="row mb-3">
<label for="fullName" class="col-md-4 col-lg-3 col-form-label">Full Name</label>
<div class="col-md-8 col-lg-9">
<input name="fullName" type="text" class="form-control" id="fullName" value="Kevin Anderson">
</div>
</div>
<div class="row mb-3">
<label for="about" class="col-md-4 col-lg-3 col-form-label">About</label>
<div class="col-md-8 col-lg-9">
<textarea name="about" class="form-control" id="about" style="height: 100px">Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.</textarea>
</div>
</div>
<div class="row mb-3">
<label for="company" class="col-md-4 col-lg-3 col-form-label">Company</label>
<div class="col-md-8 col-lg-9">
<input name="company" type="text" class="form-control" id="company" value="Lueilwitz, Wisoky and Leuschke">
</div>
</div>
<div class="row mb-3">
<label for="Job" class="col-md-4 col-lg-3 col-form-label">Job</label>
<div class="col-md-8 col-lg-9">
<input name="job" type="text" class="form-control" id="Job" value="Web Designer">
</div>
</div>
<div class="row mb-3">
<label for="Country" class="col-md-4 col-lg-3 col-form-label">Country</label>
<div class="col-md-8 col-lg-9">
<input name="country" type="text" class="form-control" id="Country" value="USA">
</div>
</div>
<div class="row mb-3">
<label for="Address" class="col-md-4 col-lg-3 col-form-label">Address</label>
<div class="col-md-8 col-lg-9">
<input name="address" type="text" class="form-control" id="Address" value="A108 Adam Street, New York, NY 535022">
</div>
</div>
<div class="row mb-3">
<label for="Phone" class="col-md-4 col-lg-3 col-form-label">Phone</label>
<div class="col-md-8 col-lg-9">
<input name="phone" type="text" class="form-control" id="Phone" value="(436) 486-3538 x29071">
</div>
</div>
<div class="row mb-3">
<label for="Email" class="col-md-4 col-lg-3 col-form-label">Email</label>
<div class="col-md-8 col-lg-9">
<input name="email" type="email" class="form-control" id="Email" value="k.anderson@example.com">
</div>
</div>
<div class="row mb-3">
<label for="Twitter" class="col-md-4 col-lg-3 col-form-label">Twitter Profile</label>
<div class="col-md-8 col-lg-9">
<input name="twitter" type="text" class="form-control" id="Twitter" value="https://twitter.com/#">
</div>
</div>
<div class="row mb-3">
<label for="Facebook" class="col-md-4 col-lg-3 col-form-label">Facebook Profile</label>
<div class="col-md-8 col-lg-9">
<input name="facebook" type="text" class="form-control" id="Facebook" value="https://facebook.com/#">
</div>
</div>
<div class="row mb-3">
<label for="Instagram" class="col-md-4 col-lg-3 col-form-label">Instagram Profile</label>
<div class="col-md-8 col-lg-9">
<input name="instagram" type="text" class="form-control" id="Instagram" value="https://instagram.com/#">
</div>
</div>
<div class="row mb-3">
<label for="Linkedin" class="col-md-4 col-lg-3 col-form-label">Linkedin Profile</label>
<div class="col-md-8 col-lg-9">
<input name="linkedin" type="text" class="form-control" id="Linkedin" value="https://linkedin.com/#">
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form><!-- End Profile Edit Form -->
</div>
<div class="tab-pane fade pt-3" id="profile-settings">
<!-- Settings Form -->
<form>
<div class="row mb-3">
<label for="fullName" class="col-md-4 col-lg-3 col-form-label">Email Notifications</label>
<div class="col-md-8 col-lg-9">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="changesMade" checked>
<label class="form-check-label" for="changesMade">
Changes made to your account
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="newProducts" checked>
<label class="form-check-label" for="newProducts">
Information on new products and services
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="proOffers">
<label class="form-check-label" for="proOffers">
Marketing and promo offers
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="securityNotify" checked disabled>
<label class="form-check-label" for="securityNotify">
Security alerts
</label>
</div>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form><!-- End settings Form -->
</div>
<div class="tab-pane fade pt-3" id="profile-change-password">
<!-- Change Password Form -->
<form>
<div class="row mb-3">
<label for="currentPassword" class="col-md-4 col-lg-3 col-form-label">Current Password</label>
<div class="col-md-8 col-lg-9">
<input name="password" type="password" class="form-control" id="currentPassword">
</div>
</div>
<div class="row mb-3">
<label for="newPassword" class="col-md-4 col-lg-3 col-form-label">New Password</label>
<div class="col-md-8 col-lg-9">
<input name="newpassword" type="password" class="form-control" id="newPassword">
</div>
</div>
<div class="row mb-3">
<label for="renewPassword" class="col-md-4 col-lg-3 col-form-label">Re-enter New Password</label>
<div class="col-md-8 col-lg-9">
<input name="renewpassword" type="password" class="form-control" id="renewPassword">
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form><!-- End Change Password Form -->
</div>
</div><!-- End Bordered Tabs -->
</div>
</div>
</div>
</div>
</section>
{% endblock main %}

View File

@ -0,0 +1,54 @@
<div class="modal fade" id="actionModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Action <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.action_add') }}" method="post">
{{ form_new_action.csrf_token }}
<div class="modal-body">
{% for field in form_new_action %}
{% if field != form_new_action.csrf_token %}
{% if field == form_new_action.devices %}
<div class="col-12">
{{ field.label(class_="form-label") }}: <span class="devices-count"></span>
{{ field(class_="devicesList") }}
<p class="text-danger pol">
You need select first some device before to do one action
</p>
<p class="enumeration-devices"></p>
</div>
{% elif field == form_new_action.lot %}
{{ field }}
{% elif field == form_new_action.type %}
{{ field }}
{% else %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{{ field(class_="form-control") }}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endfor %}
</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="Create" />
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,33 @@
<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

@ -0,0 +1,33 @@
<div class="modal fade" id="addingTagModal" 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 tag</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.tag_devices_add') }}" method="post">
{{ form_tag_device.csrf_token }}
<div class="modal-body">
Please write a name of a tag
<select class="form-control selectpicker" id="selectTag" name="tag" data-live-search="true">
{% for tag in tags %}
<option value="{{ tag.id }}">{{ tag.id }}</option>
{% endfor %}
</select>
<input class="devicesList" type="hidden" name="device" />
<p class="text-danger pol">
You need select first one device and only one for add this in a tag
</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

@ -0,0 +1,21 @@
<div class="modal fade" id="exportErrorModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Error export</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-danger pol">
You need select first some device for use export file
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
<div class="modal fade" id="unlinkTagErrorModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Error Unlink Tag</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-danger pol">
You need select first one device and only one for Unlink one Tag
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,55 @@
<div class="modal fade" id="allocateModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="{{ form_new_allocate.check_valid() }}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Action <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.allocate_add') }}" method="post">
{{ form_new_allocate.csrf_token }}
<div class="modal-body">
{% for field in form_new_allocate %}
{% if field != form_new_allocate.csrf_token %}
{% if field == form_new_allocate.devices %}
<div class="col-12">
{{ field.label(class_="form-label") }}: <span class="devices-count"></span>
{{ field(class_="devicesList") }}
<p class="text-danger pol" style="display: none;">
You need select first some device before to do one action
</p>
<p class="enumeration-devices"></p>
</div>
{% elif field == form_new_allocate.lot %}
{{ field }}
{% elif field == form_new_allocate.type %}
{{ field }}
{% else %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{{ field(class_="form-control") }}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endfor %}
</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="Create" />
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,85 @@
<div class="modal fade" id="datawipeModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="{{ form_new_datawipe.check_valid() }}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Action <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.datawipe_add') }}" method="post" enctype="multipart/form-data">
{{ form_new_datawipe.csrf_token }}
<div class="modal-body">
{% for field in form_new_datawipe %}
{% if field != form_new_datawipe.csrf_token %}
{% if field == form_new_datawipe.devices %}
<div class="col-12">
{{ field.label(class_="form-label") }}: <span class="devices-count"></span>
{{ field(class_="devicesList") }}
<p class="text-danger pol" style="display: none;">
You need select first some device before to do one action
</p>
<p class="enumeration-devices"></p>
</div>
{% elif field == form_new_datawipe.lot %}
{{ field }}
{% elif field == form_new_datawipe.type %}
{{ field }}
{% elif field == form_new_datawipe.document %}
{% for _field in field %}
<div class="col-12">
{{ _field.label(class_="form-label") }}
{% if _field == field.success %}
<div class="form-check form-switch">
{{ _field(class_="form-check-input") }}
<small class="text-muted">{{ _field.description }}</small>
</div>
{% else %}
{{ _field(class_="form-control") }}
<small class="text-muted">{{ _field.description }}</small>
{% endif %}
{% if _field.errors %}
<p class="text-danger">
{% for error in _field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{% if field == form_new_datawipe.success %}
<div class="form-check form-switch">
{{ field(class_="form-check-input") }}
<small class="text-muted">{{ field.description }}</small>
</div>
{% else %}
{{ field(class_="form-control") }}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endfor %}
</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="Create" />
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,393 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ page_title }}</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
<li class="breadcrumb-item">{{ page_title }}</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">
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<form method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
<div>
<div class="form-group has-validation mb-2">
<label for="name" class="form-label">Type *</label>
<select id="type" class="form-control" name="type" required="">
<option value="">Select one Type</option>
<optgroup label="Computer Monitor">
<option value="Monitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Monitor</option>
</optgroup>
<optgroup label="Mobile">
<option value="Smartphone"
{% if form.type.data == 'Smartphone' %} selected="selected"{% endif %}>Smartphone</option>
<option value="Tablet"
{% if form.type.data == 'Tablet' %} selected="selected"{% endif %}>Tablet</option>
<option value="Cellphone"
{% if form.type.data == 'Cellphone' %} selected="selected"{% endif %}>Cellphone</option>
</optgroup>
<optgroup label="Computer Accessory">
<option value="Mouse"
{% if form.type.data == 'Mouse' %} selected="selected"{% endif %}>Mouse</option>
<option value="MemoryCardReader"
{% if form.type.data == 'MemoryCardReader' %} selected="selected"{% endif %}>Memory card reader</option>
<option value="SAI"
{% if form.type.data == 'SAI' %} selected="selected"{% endif %}>SAI</option>
<option value="Keyboard"
{% if form.type.data == 'Keyboard' %} selected="selected"{% endif %}>Keyboard</option>
</optgroup>
</select>
<small class="text-muted form-text">Type of devices</small>
{% if form.type.errors %}
<p class="text-danger">
{% for error in form.type.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="form-group mb-2">
<label for="label" class="form-label">Label</label>
{{ form.label(class_="form-control") }}
<small class="text-muted form-text">Label that you want link to this device</small>
{% if form.label.errors %}
<p class="text-danger">
{% for error in form.label.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="serialNumber" class="form-label">{{ form.serial_number.label }} *</label>
{{ form.serial_number(class_="form-control") }}
<small class="text-muted form-text">Serial number of this device</small>
{% if form.serial_number.errors %}
<p class="text-danger">
{% for error in form.serial_number.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.model.label }} *</label>
{{ form.model(class_="form-control") }}
<small class="text-muted form-text">Name of model</small>
{% if form.model.errors %}
<p class="text-danger">
{% for error in form.model.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.manufacturer.label }} *</label>
{{ form.manufacturer(class_="form-control") }}
<small class="text-muted form-text">Name of manufacturer</small>
{% if form.manufacturer.errors %}
<p class="text-danger">
{% for error in form.manufacturer.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.appearance.label }}</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="Z">
<label class="form-check-label">0. The device is new.</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="A">
<label class="form-check-label">A. Like new (no visual damage))</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="B">
<label class="form-check-label">B. In very good condition (small visual damage to hard-to-detect parts)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="C">
<label class="form-check-label">C. In good condition (small visual damage to easy-to-detect parts, not the screen))</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="D">
<label class="form-check-label">D. It is acceptable (visual damage to visible parts, not on the screen)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="E">
<label class="form-check-label">E. It is unacceptable (substantial visual damage that may affect use)</label>
</div>
<small class="text-muted form-text">Rate the imperfections that affect the device aesthetically, but not its use.</small>
{% if form.appearance.errors %}
<p class="text-danger">
{% for error in form.appearance.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.functionality.label }}</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="A">
<label class="form-check-label">A. Everything works perfectly (buttons, and no scratches on the screen)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="B">
<label class="form-check-label">B. There is a hard to press button or small scratches on the corners of the screen</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="C">
<label class="form-check-label">C. A non-essential button does not work; the screen has multiple scratches on the corners</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="D">
<label class="form-check-label">D. Multiple buttons do not work properly; the screen has severe damage that may affect use</label>
</div>
<small class="text-muted form-text">It qualifies the defects of a device that affect its use.</small>
{% if form.functionality.errors %}
<p class="text-danger">
{% for error in form.functionality.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.brand.label }}</label>
{{ form.brand(class_="form-control") }}
<small class="text-muted form-text">A naming for consumers. This field can represent several models, so it can be ambiguous, and it is not used to identify the product.</small>
{% if form.brand.errors %}
<p class="text-danger">
{% for error in form.brand.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.generation.label }} *</label>
{{ form.generation(class_="form-control") }}
<small class="text-muted form-text">The generation of the device.</small>
{% if form.generation.errors %}
<p class="text-danger">
{% for error in form.generation.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.version.label }}</label>
{{ form.version(class_="form-control") }}
<small class="text-muted form-text">The version code o fthis device, like 'v1' or 'A001'.</small>
{% if form.version.errors %}
<p class="text-danger">
{% for error in form.version.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.weight.label }} *</label>
{{ form.weight(class_="form-control") }}
<small class="text-muted form-text">The weight of the device in Kg.</small>
{% if form.weight.errors %}
<p class="text-danger">
{% for error in form.weight.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.width.label }} *</label>
{{ form.width(class_="form-control") }}
<small class="text-muted form-text">The width of the device in meters.</small>
{% if form.width.errors %}
<p class="text-danger">
{% for error in form.width.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.height.label }} *</label>
{{ form.height(class_="form-control") }}
<small class="text-muted form-text">The height of the device in meters.</small>
{% if form.height.errors %}
<p class="text-danger">
{% for error in form.height.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.depth.label }} *</label>
{{ form.depth(class_="form-control") }}
<small class="text-muted form-text">The depth of the device in meters.</small>
{% if form.depth.errors %}
<p class="text-danger">
{% for error in form.depth.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.variant.label }}</label>
{{ form.variant(class_="form-control") }}
<small class="text-muted form-text">A variant or sub-model of the device.</small>
{% if form.variant.errors %}
<p class="text-danger">
{% for error in form.variant.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.sku.label }}</label>
{{ form.sku(class_="form-control") }}
<small class="text-muted form-text">The Stock Keeping Unit (SKU), i.e. a merchant-specific identifier for a product or service.</small>
{% if form.sku.errors %}
<p class="text-danger">
{% for error in form.sku.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2">
<label for="image" class="form-label">{{ form.image.label }}</label>
{{ form.image(class_="form-control") }}
<small class="text-muted form-text">An URL containing an image of the device.</small>
{% if form.image.errors %}
<p class="text-danger">
{% for error in form.image.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div id="imei" class="from-group has-validation mb-2">
<label for="imei" class="form-label">{{ form.imei.label }}</label>
{{ form.imei(class_="form-control") }}
<small class="text-muted form-text">A number from 14 to 16 digits.</small>
{% if form.imei.errors %}
<p class="text-danger">
{% for error in form.imei.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div id="meid" class="from-group has-validation mb-2">
<label for="meid" class="form-label">{{ form.meid.label }}</label>
{{ form.meid(class_="form-control") }}
<small class="text-muted form-text">14 hexadecimal digits.</small>
{% if form.meid.errors %}
<p class="text-danger">
{% for error in form.meid.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div id="resolution" class="from-group has-validation mb-2">
<label for="resolution" class="form-label">{{ form.resolution.label }}</label>
{{ form.resolution(class_="form-control") }}
<small class="text-muted form-text">The resolution width of the screen.</small>
{% if form.resolution.errors %}
<p class="text-danger">
{% for error in form.resolution.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div id="screen" class="from-group has-validation mb-2">
<label for="screen" class="form-label">{{ form.screen.label }}</label>
{{ form.screen(class_="form-control") }}
<small class="text-muted form-text">The size of the screen.</small>
{% if form.screen.errors %}
<p class="text-danger">
{% for error in form.screen.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
</div>
<div>
{% if lot_id %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot_id) }}" class="btn btn-danger">Cancel</a>
{% else %}
<a href="{{ url_for('inventory.devicelist') }}" class="btn btn-danger">Cancel</a>
{% endif %}
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
<script src="{{ url_for('static', filename='js/create_device.js') }}"></script>
{% endblock main %}

View File

@ -0,0 +1,148 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
<li class="breadcrumb-item active">{{ page_title }}</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-body pt-3">
<h3>{{ device.devicehub_id }}</h3>
<!-- Bordered Tabs -->
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#type">Type</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#status">Status</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#rate">Rate</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#traceability">Traceability log</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button>
</li>
</ul>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="type">
<h5 class="card-title">Type Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Type</div>
<div class="col-lg-9 col-md-8">{{ device.type }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Manufacturer</div>
<div class="col-lg-9 col-md-8">{{ device.manufacturer }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Model</div>
<div class="col-lg-9 col-md-8">{{ device.model }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Serial Number</div>
<div class="col-lg-9 col-md-8">{{ device.serial_number }}</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="status">
<h5 class="card-title">Status Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label">Physical States</div>
<div class="col-lg-9 col-md-8">{{ device.physical or '' }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Trading States</div>
<div class="col-lg-9 col-md-8">{{ device.last_action_trading or ''}}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Usage States</div>
<div class="col-lg-9 col-md-8">{{ device.usage or '' }}</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="rate">
<h5 class="card-title">Rate Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label">Rating</div>
<div class="col-lg-9 col-md-8">{{ device.rate or '' }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Processor</div>
<div class="col-lg-9 col-md-8">{{ device.rate.processor or '' }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">RAM</div>
<div class="col-lg-9 col-md-8">{{ device.rate.ram or '' }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Data storage</div>
<div class="col-lg-9 col-md-8">{{ device.rate.data_storage or '' }}</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="traceability">
<h5 class="card-title">Traceability log Details</h5>
<div class="list-group col-6">
{% for action in device.reverse_actions %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ action.type }} {{ action.severity }}
<small class="text-muted">{{ action.created.strftime('%H:%M %d-%m-%Y') }}</small>
</div>
{% endfor %}
</div>
</div>
<div class="tab-pane fade profile-overview" id="components">
<h5 class="card-title">Components Details</h5>
<div class="list-group col-6">
{% for component in device.components|sort(attribute='type') %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ component.type }}</h5>
<small class="text-muted">{{ component.created.strftime('%H:%M %d-%m-%Y') }}</small>
</div>
<p class="mb-1">
{{ component.manufacturer }}<br />
{{ component.model }}<br />
</p>
<small class="text-muted">
{% if component.type in ['RamModule', 'HardDrive', 'SolidStateDrive'] %}
{{ component.size }}MB
{% endif %}
</small>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock main %}

View File

@ -0,0 +1,405 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
{% if not lot %}
<li class="breadcrumb-item active">Unassgined</li>
{% elif lot.is_temporary %}
<li class="breadcrumb-item active">Temporary Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% elif lot.is_incoming %}
<li class="breadcrumb-item active">Incoming Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% elif lot.is_outgoing %}
<li class="breadcrumb-item active">Outgoing Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% endif %}
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-12">
{% if lot %}
<div class="card">
<div class="card-body pt-3">
<!-- Bordered Tabs -->
<div class="d-flex align-items-center justify-content-between">
<h3><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3>
<div><!-- lot actions -->
{% if lot.is_temporary %}
<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')">
<i class="bi bi-arrow-down-right"></i> Add supplier
</a>
<a href="javascript:newTrade('user_to')">
<i class="bi bi-arrow-up-right"></i> Add receiver
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-body pt-3" style="min-height: 650px;">
<!-- Bordered Tabs -->
{% if lot and not lot.is_temporary %}
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#devices-list">Devices</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button>
</li>
</ul>
{% endif %}
<div class="tab-content pt-5">
<div id="devices-list" class="tab-pane fade devices-list active show">
<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">
<i class="bi bi-folder2"></i>
Lots
<span class="caret"></span>
</button>
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnLots">
<li>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addingLotModal">
<i class="bi bi-plus"></i>
Add selected Devices to a lot
</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>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus"></i>
New Actions
</button>
<span class="d-none" id="activeActionModal" data-bs-toggle="modal" data-bs-target="#actionModal"></span>
<span class="d-none" id="activeAllocateModal" data-bs-toggle="modal" data-bs-target="#allocateModal"></span>
<span class="d-none" id="activeDatawipeModal" data-bs-toggle="modal" data-bs-target="#datawipeModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnActions">
<li>
Status actions
</li>
<li>
<a href="javascript:newAction('Recycling')" class="dropdown-item">
<i class="bi bi-recycle"></i>
Recycling
</a>
</li>
<li>
<a href="javascript:newAction('Use')" class="dropdown-item">
<i class="bi bi-play-circle-fill"></i>
Use
</a>
</li>
<li>
<a href="javascript:newAction('Refurbish')" class="dropdown-item">
<i class="bi bi-tools"></i>
Refurbish
</a>
</li>
<li>
<a href="javascript:newAction('Management')" class="dropdown-item">
<i class="bi bi-mastodon"></i>
Management
</a>
</li>
<li>
Allocation
</li>
<li>
<a href="javascript:newAllocate('Allocate')" class="dropdown-item">
<i class="bi bi-house-fill"></i>
Allocate
</a>
</li>
<li>
<a href="javascript:newAllocate('Deallocate')" class="dropdown-item">
<i class="bi bi-house"></i>
Deallocate
</a>
</li>
<li>
Physical actions
</li>
<li>
<a href="javascript:newAction('ToPrepare')" class="dropdown-item">
<i class="bi bi-tools"></i>
ToPrepare
</a>
</li>
<li>
<a href="javascript:newAction('Prepare')" class="dropdown-item">
<i class="bi bi-egg"></i>
Prepare
</a>
</li>
<li>
<a href="javascript:newDataWipe('DataWipe')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
DataWipe
</a>
</li>
<li>
<a href="javascript:newAction('ToRepair')" class="dropdown-item">
<i class="bi bi-screwdriver"></i>
ToRepair
</a>
</li>
<li>
<a href="javascript:newAction('Ready')" class="dropdown-item">
<i class="bi bi-check2-all"></i>
Ready
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-reply"></i>
Exports
</button>
<span class="d-none" id="exportAlertModal" data-bs-toggle="modal" data-bs-target="#exportErrorModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnExport">
<li>
<a href="javascript:export_file('devices')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i>
Devices Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('metrics')" class="dropdown-item">
<i class="bi bi-file-spreadsheet"></i>
Metrics Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('links')" class="dropdown-item">
<i class="bi bi-link-45deg"></i>
Public Links
</a>
</li>
<li>
<a href="javascript:export_file('certificates')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
Erasure Certificate
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i>
Tags
</button>
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnTags">
<li>
<a href="javascript:addTag()" class="dropdown-item">
<i class="bi bi-plus"></i>
Add Tag to selected Device
</a>
</li>
<li>
<a href="javascript:removeTag()" class="dropdown-item">
<i class="bi bi-x"></i>
Remove Tag from selected Device
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-laptop"></i>
New Device
</button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li>
{% if lot %}
<a href="{{ url_for('inventory.lot_upload_snapshot', lot_id=lot.id) }}" class="dropdown-item">
{% else %}
<a href="{{ url_for('inventory.upload_snapshot') }}" class="dropdown-item">
{% endif %}
<i class="bi bi-plus"></i>
Upload a new Snapshot
</a>
</li>
<li>
{% if lot %}
<a href="{{ url_for('inventory.lot_device_add', lot_id=lot.id) }}" class="dropdown-item">
{% else %}
<a href="{{ url_for('inventory.device_add') }}" class="dropdown-item">
{% endif %}
<i class="bi bi-plus"></i>
Create a new Device
</a>
</li>
</ul>
</div>
{% if lot and not lot.is_temporary %}
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-book"></i>
Documents
</button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li>
<a href="{{ url_for('inventory.trade_document_add', lot_id=lot.id)}}" class="dropdown-item">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</li>
</ul>
</div>
{% endif %}
<div class="tab-content pt-2">
<form method="get">
<div class="d-flex mt-4 mb-4">
{% for f in form_filter %}
{{ f }}
{% endfor %}
<input type="submit" class="ms-2 btn btn-primary" value="Filter" />
</div>
</form>
<p class="mt-3">
Displaying devices of type
<em>{{ form_filter.filter.data or "Computer" }}</em>
</p>
<table class="table">
<thead>
<tr>
<th scope="col">Select</th>
<th scope="col">Title</th>
<th scope="col">DHID</th>
<th scope="col">Tags</th>
<th scope="col">Status</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Update</th>
</tr>
</thead>
<tbody>
{% for dev in devices %}
<tr>
<td>
<input type="checkbox" class="deviceSelect" data="{{ dev.id }}"
data-device-type="{{ dev.type }}" data-device-manufacturer="{{ dev.manufacturer }}"
data-device-dhid="{{ dev.devicehub_id }}"
{% if form_new_allocate.type.data and dev.id in list_devices %}
checked="checked"
{% endif %}
/>
</td>
<td>
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{{ dev.verbose_name }}
</a>
</td>
<td>
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
{{ dev.devicehub_id }}
</a>
</td>
<td>
{% for t in dev.tags | sort(attribute="id") %}
<a href="{{ url_for('inventory.tag_details', id=t.id)}}">{{ t.id }}</a>
{% if not loop.last %},{% endif %}
{% endfor %}
</td>
<td>{% if dev.status %}{{ dev.status }}{% endif %}</td>
<td>{{ dev.updated.strftime('%H:%M %d-%m-%Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if lot and not lot.is_temporary %}
<div id="trade-documents-list" class="tab-pane fade trade-documents-list">
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Uploaded on</th>
</tr>
</thead>
<tbody>
{% for doc in lot.trade.documents %}
<tr>
<td>
{% if doc.url.to_text() %}
<a href="{{ doc.url.to_text() }}" target="_blank">{{ doc.file_name}}</a>
{% else %}
{{ doc.file_name}}
{% endif %}
</td>
<td>
{{ doc.created.strftime('%H:%M %d-%m-%Y')}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div><!-- End Bordered Tabs -->
</div>
</div>
</div>
</div>
</div>
</section>
{% include "inventory/addDeviceslot.html" %}
{% include "inventory/addDevicestag.html" %}
{% include "inventory/removeDeviceslot.html" %}
{% include "inventory/removelot.html" %}
{% include "inventory/actions.html" %}
{% include "inventory/allocate.html" %}
{% include "inventory/data_wipe.html" %}
{% include "inventory/trade.html" %}
{% include "inventory/alert_export_error.html" %}
{% include "inventory/alert_unlink_tag_error.html" %}
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table")
</script>
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
{% endblock main %}

View File

@ -0,0 +1,92 @@
{% extends "documents/layout.html" %}
{% block body %}
<div>
<h2>Summary</h2>
<table class="table table-bordered">
<thead>
<tr>
<th>S/N Data Storage</th>
<th>Type of erasure</th>
<th>Result</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for erasure in erasures %}
<tr>
<td>
{{ erasure.device.serial_number.upper() }}
</td>
<td>
{{ erasure.type }}
</td>
<td>
{{ erasure.severity }}
</td>
<td>
{{ erasure.date_str }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="page-break row">
<h2>Details</h2>
{% for erasure in erasures %}
<div class="col-md-6 no-page-break">
<h4>{{ erasure.device.__format__('t') }}</h4>
<dl>
<dt>Data storage:</dt>
<dd>{{ erasure.device.__format__('ts') }}</dd>
<dt>Computer where was erase:</dt>
<dd>Title: {{ erasure.parent.__format__('ts') }}</dd>
<dd>DevicehubID: {{ erasure.parent.devicehub_id }}</dd>
<dd>Hid: {{ erasure.parent.hid }}</dd>
<dd>Tags: {{ erasure.parent.tags }}</dd>
<dt>Computer where it resides:</dt>
<dd>Title: {{ erasure.device.parent.__format__('ts') }}</dd>
<dd>DevicehubID: {{ erasure.device.parent.devicehub_id }}</dd>
<dd>Hid: {{ erasure.device.parent.hid }}</dd>
<dd>Tags: {{ erasure.device.parent.tags }}</dd>
<dt>Erasure:</dt>
<dd>{{ erasure.__format__('ts') }}</dd>
{% if erasure.steps %}
<dt>Erasure steps:</dt>
<dd>
<ol>
{% for step in erasure.steps %}
<li>{{ step.__format__('') }}</li>
{% endfor %}
</ol>
</dd>
{% endif %}
</dl>
</div>
{% endfor %}
</div>
<div class="no-page-break">
<h2>Glossary</h2>
<dl>
<dt>Erase Basic</dt>
<dd>
A software-based fast non-100%-secured way of erasing data storage,
using <a href="https://en.wikipedia.org/wiki/Shred_(Unix)">shred</a>.
</dd>
<dt>Erase Sectors</dt>
<dd>
A secured-way of erasing data storages, checking sector-by-sector
the erasure, using <a href="https://en.wikipedia.org/wiki/Badblocks">badblocks</a>.
</dd>
</dl>
</div>
<div class="no-print">
<a href="{{ url_pdf }}">Click here to download the PDF.</a>
</div>
<div class="print-only">
<a href="{{ url_for('Document.StampsView', _external=True) }}">Verify on-line the integrity of this document</a>
</div>
{% endblock %}

View File

@ -0,0 +1,65 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<!-- TODO@slamora replace with lot list URL when exists -->
<li class="breadcrumb-item"><a href="#TODO-lot-list">Lots</a></li>
<li class="breadcrumb-item">Lot</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-4">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
<p class="text-center small">Please enter a name of the lot.</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<form method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
<div>
<label for="name" class="form-label">Name</label>
<div class="input-group has-validation">
<input type="text" name="name" class="form-control" required value="{{ form.name.data|default('', true) }}">
<div class="invalid-feedback">Please enter a name of the lot.</div>
</div>
</div>
<div>
{% if form.id %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=form.id) }}" class="btn btn-danger">Cancel</a>
{% else %}
<a href="{{ url_for('inventory.devicelist') }}" class="btn btn-danger">Cancel</a>
{% endif %}
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
{% endblock main %}

View File

@ -0,0 +1,32 @@
<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

@ -0,0 +1,26 @@
<div class="modal fade" id="btnRemoveLots" 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 Lot</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure that you want to remove lot <strong>{{ lot.name }}</strong>?
<p class="text-danger">
There are devices in this lot, are you sure you want to delete it?
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<a href="{{ url_for('inventory.lot_del', id=lot.id)}}" type="button" class="btn btn-primary">
Confirm
</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,67 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.taglist')}}">Tag management</a></li>
<li class="breadcrumb-item">{{ page_title }}</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-4">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Add a new Tag</h5>
<p class="text-center small">Please enter a code for the tag.</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<form method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
<div>
<label for="code" class="form-label">code</label>
<div class="input-group has-validation">
<input type="text" name="code" class="form-control" required value="{{ form.code.data|default('', true) }}">
<div class="invalid-feedback">Please enter a code of the tag.</div>
</div>
{% if form.code.errors %}
<p class="text-danger">
{% for error in form.code.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div>
<a href="{{ url_for('inventory.taglist') }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
{% endblock main %}

Some files were not shown because too many files have changed in this diff Show More