Merge pull request #447 from eReuse/feature/4343-share-lot

Feature/4343 share lot
This commit is contained in:
cayop 2023-04-28 15:10:29 +02:00 committed by GitHub
commit 8ba853b14a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 215 additions and 19 deletions

View File

@ -70,7 +70,7 @@ from ereuse_devicehub.resources.device.models import (
from ereuse_devicehub.resources.documents.models import DataWipeDocument from ereuse_devicehub.resources.documents.models import DataWipeDocument
from ereuse_devicehub.resources.enums import Severity from ereuse_devicehub.resources.enums import Severity
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot, ShareLot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
@ -160,11 +160,14 @@ class FilterForm(FlaskForm):
'', choices=DEVICES, default="All Computers", render_kw={'class': "form-select"} '', choices=DEVICES, default="All Computers", render_kw={'class': "form-select"}
) )
def __init__(self, lots, lot_id, *args, **kwargs): def __init__(self, lots, lot, lot_id, *args, **kwargs):
self.all_devices = kwargs.pop('all_devices', False) self.all_devices = kwargs.pop('all_devices', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.lots = lots self.lots = lots
self.lot = lot
self.lot_id = lot_id self.lot_id = lot_id
if self.lot_id and not self.lot:
self.lot = self.lots.filter(Lot.id == self.lot_id).one()
self._get_types() self._get_types()
def _get_types(self): def _get_types(self):
@ -175,8 +178,7 @@ class FilterForm(FlaskForm):
self.filter.data = self.device_type self.filter.data = self.device_type
def filter_from_lots(self): def filter_from_lots(self):
if self.lot_id: if self.lot:
self.lot = self.lots.filter(Lot.id == self.lot_id).one()
device_ids = (d.id for d in self.lot.devices) device_ids = (d.id for d in self.lot.devices)
self.devices = Device.query.filter(Device.id.in_(device_ids)).filter( self.devices = Device.query.filter(Device.id.in_(device_ids)).filter(
Device.binding == None # noqa: E711 Device.binding == None # noqa: E711
@ -256,7 +258,8 @@ class LotForm(FlaskForm):
return self.id return self.id
def remove(self): def remove(self):
if self.instance and not self.instance.trade: shared = ShareLot.query.filter_by(lot=self.instance).first()
if self.instance and not self.instance.trade and not shared:
self.instance.delete() self.instance.delete()
db.session.commit() db.session.commit()
return self.instance return self.instance

View File

@ -14,6 +14,7 @@ from flask import current_app as app
from flask import g, make_response, request, url_for from flask import g, make_response, request, url_for
from flask.views import View from flask.views import View
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy import or_
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from ereuse_devicehub import messages from ereuse_devicehub import messages
@ -51,7 +52,7 @@ from ereuse_devicehub.resources.device.models import (
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.enums import SnapshotSoftware
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot, ShareLot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.views import GenericMixin from ereuse_devicehub.views import GenericMixin
@ -73,19 +74,25 @@ class DeviceListMixin(GenericMixin):
per_page = int(request.args.get('per_page', PER_PAGE)) per_page = int(request.args.get('per_page', PER_PAGE))
filter = request.args.get('filter', "All+Computers") filter = request.args.get('filter', "All+Computers")
lot = None
share_lots = self.context['share_lots']
share_lot = share_lots.filter_by(lot_id=lot_id).first()
if share_lot:
lot = share_lot.lot
lots = self.context['lots'] lots = self.context['lots']
form_filter = FilterForm(lots, lot_id, all_devices=all_devices) form_filter = FilterForm(lots, lot, lot_id, all_devices=all_devices)
devices = form_filter.search().paginate(page=page, per_page=per_page) devices = form_filter.search().paginate(page=page, per_page=per_page)
devices.first = per_page * devices.page - per_page + 1 devices.first = per_page * devices.page - per_page + 1
devices.last = len(devices.items) + devices.first - 1 devices.last = len(devices.items) + devices.first - 1
lot = None
form_transfer = '' form_transfer = ''
form_delivery = '' form_delivery = ''
form_receiver = '' form_receiver = ''
form_customer_details = '' form_customer_details = ''
if lot_id: if lot_id and not lot:
lot = lots.filter(Lot.id == lot_id).one() lot = lots.filter(Lot.id == lot_id).one()
if not lot.is_temporary and lot.transfer: if not lot.is_temporary and lot.transfer:
form_transfer = EditTransferForm(lot_id=lot.id) form_transfer = EditTransferForm(lot_id=lot.id)
@ -111,6 +118,7 @@ class DeviceListMixin(GenericMixin):
'list_devices': self.get_selected_devices(form_new_action), 'list_devices': self.get_selected_devices(form_new_action),
'all_devices': all_devices, 'all_devices': all_devices,
'filter': filter, 'filter': filter,
'share_lots': share_lots,
} }
) )
@ -537,8 +545,9 @@ class LotDeleteView(View):
def dispatch_request(self, id): def dispatch_request(self, id):
form = LotForm(id=id) form = LotForm(id=id)
if form.instance.trade: shared = ShareLot.query.filter_by(lot=form.instance).first()
msg = "Sorry, the lot cannot be deleted because have a trade action " if form.instance.trade or shared:
msg = "Sorry, the lot cannot be deleted because this lot is share"
messages.error(msg) messages.error(msg)
next_url = url_for('inventory.lotdevicelist', lot_id=id) next_url = url_for('inventory.lotdevicelist', lot_id=id)
return flask.redirect(next_url) return flask.redirect(next_url)
@ -1005,9 +1014,21 @@ class ExportsView(View):
return export_ids[export_id]() return export_ids[export_id]()
def find_devices(self): def find_devices(self):
# import pdb; pdb.set_trace()
sql = """
select lot_device.device_id as id from {schema}.share_lot as share
inner join {schema}.lot_device as lot_device
on share.lot_id=lot_device.lot_id
where share.user_to_id='{user_id}'
""".format(
schema=app.config.get('SCHEMA'), user_id=g.user.id
)
shared = (x[0] for x in db.session.execute(sql))
args = request.args.get('ids') args = request.args.get('ids')
ids = args.split(',') if args else [] ids = args.split(',') if args else []
query = Device.query.filter(Device.owner == g.user) query = Device.query.filter(or_(Device.owner == g.user, Device.id.in_(shared)))
return query.filter(Device.devicehub_id.in_(ids)) return query.filter(Device.devicehub_id.in_(ids))
def response_csv(self, data, name): def response_csv(self, data, name):

View File

@ -8,7 +8,7 @@ from requests.exceptions import ConnectionError
from ereuse_devicehub import __version__, messages from ereuse_devicehub import __version__, messages
from ereuse_devicehub.labels.forms import PrintLabelsForm, TagForm, TagUnnamedForm from ereuse_devicehub.labels.forms import PrintLabelsForm, TagForm, TagUnnamedForm
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot, ShareLot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
labels = Blueprint('labels', __name__, url_prefix='/labels') labels = Blueprint('labels', __name__, url_prefix='/labels')
@ -23,6 +23,7 @@ class TagListView(View):
def dispatch_request(self): def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id) lots = Lot.query.filter(Lot.owner_id == current_user.id)
share_lots = ShareLot.query.filter_by(user_to_id=current_user.id)
tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by( tags = Tag.query.filter(Tag.owner_id == current_user.id).order_by(
Tag.created.desc() Tag.created.desc()
) )
@ -31,6 +32,7 @@ class TagListView(View):
'tags': tags, 'tags': tags,
'page_title': 'Unique Identifiers Management', 'page_title': 'Unique Identifiers Management',
'version': __version__, 'version': __version__,
'share_lots': share_lots,
} }
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
@ -42,7 +44,13 @@ class TagAddView(View):
def dispatch_request(self): def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id) lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {'page_title': 'New Tag', 'lots': lots, 'version': __version__} share_lots = ShareLot.query.filter_by(user_to_id=current_user.id)
context = {
'page_title': 'New Tag',
'lots': lots,
'version': __version__,
'share_lots': share_lots,
}
form = TagForm() form = TagForm()
if form.validate_on_submit(): if form.validate_on_submit():
form.save() form.save()
@ -59,10 +67,12 @@ class TagAddUnnamedView(View):
def dispatch_request(self): def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id) lots = Lot.query.filter(Lot.owner_id == current_user.id)
share_lots = ShareLot.query.filter_by(user_to_id=current_user.id)
context = { context = {
'page_title': 'New Unnamed Tag', 'page_title': 'New Unnamed Tag',
'lots': lots, 'lots': lots,
'version': __version__, 'version': __version__,
'share_lots': share_lots,
} }
form = TagUnnamedForm() form = TagUnnamedForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -94,11 +104,13 @@ class PrintLabelsView(View):
def dispatch_request(self): def dispatch_request(self):
lots = Lot.query.filter(Lot.owner_id == current_user.id) lots = Lot.query.filter(Lot.owner_id == current_user.id)
share_lots = ShareLot.query.filter_by(user_to_id=current_user.id)
context = { context = {
'lots': lots, 'lots': lots,
'page_title': self.title, 'page_title': self.title,
'version': __version__, 'version': __version__,
'referrer': request.referrer, 'referrer': request.referrer,
'share_lots': share_lots,
} }
form = PrintLabelsForm() form = PrintLabelsForm()
@ -123,6 +135,7 @@ class LabelDetailView(View):
def dispatch_request(self, id): def dispatch_request(self, id):
lots = Lot.query.filter(Lot.owner_id == current_user.id) lots = Lot.query.filter(Lot.owner_id == current_user.id)
share_lots = ShareLot.query.filter_by(user_to_id=current_user.id)
tag = ( tag = (
Tag.query.filter(Tag.owner_id == current_user.id).filter(Tag.id == id).one() Tag.query.filter(Tag.owner_id == current_user.id).filter(Tag.id == id).one()
) )
@ -131,6 +144,7 @@ class LabelDetailView(View):
'page_title': self.title, 'page_title': self.title,
'version': __version__, 'version': __version__,
'referrer': request.referrer, 'referrer': request.referrer,
'share_lots': share_lots,
} }
devices = [] devices = []

View File

@ -0,0 +1,52 @@
"""share lot
Revision ID: 2f2ef041483a
Revises: ac476b60d952
Create Date: 2023-04-26 16:04:21.560888
"""
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '2f2ef041483a'
down_revision = 'ac476b60d952'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'share_lot',
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_to_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['user_to_id'], ['common.user.id']),
sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id']),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
def downgrade():
op.drop_table('share_lot', schema=f'{get_inv()}')

View File

@ -124,7 +124,10 @@ class Lot(Thing):
@property @property
def is_temporary(self): def is_temporary(self):
return not bool(self.trade) and not bool(self.transfer) trade = bool(self.trade)
transfer = bool(self.transfer)
owner = self.owner == g.user
return not trade and not transfer and owner
@property @property
def is_incoming(self): def is_incoming(self):
@ -144,6 +147,19 @@ class Lot(Thing):
return False return False
@property
def is_shared(self):
try:
self.shared
except Exception:
self.shared = ShareLot.query.filter_by(
lot_id=self.id, user_to=g.user
).first()
if self.shared:
return True
return False
@classmethod @classmethod
def descendantsq(cls, id): def descendantsq(cls, id):
_id = UUIDLtree.convert(id) _id = UUIDLtree.convert(id)
@ -396,3 +412,15 @@ class LotParent(db.Model):
.select_from(Path) .select_from(Path)
.where(i > 0), .where(i > 0),
) )
class ShareLot(Thing):
id = db.Column(UUID(as_uuid=True), primary_key=True)
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False)
lot = db.relationship(Lot, primaryjoin=lot_id == Lot.id)
user_to_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=True,
)
user_to = db.relationship(User, primaryjoin=user_to_id == User.id)

View File

@ -276,7 +276,28 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</li>
{% if share_lots.all() %}
<li class="nav-item">
<a class="nav-link collapsed" data-bs-target="#share-lots-nav" data-bs-toggle="collapse" href="javascript:void()">
<i class="bi bi-share-fill"></i><span>Shared with me</span><i
class="bi bi-chevron-down ms-auto"></i>
</a>
{% if lot.is_shared %}
<ul id="share-lots-nav" class="nav-content collapse show" data-bs-parent="#sidebar-nav">
{% else %}
<ul id="share-lots-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
{% endif %}
{% for lot in share_lots %}
<li>
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot.lot_id) }}">
<i class="bi bi-circle"></i><span>{{ lot.lot.name }}</span>
</a>
</li>
{% endfor %}
</ul>
</li><!-- End Temporal Lots Nav --> </li><!-- End Temporal Lots Nav -->
{% endif %}
</ul> </ul>

View File

@ -21,6 +21,9 @@
{% elif lot.is_outgoing %} {% elif lot.is_outgoing %}
<li class="breadcrumb-item active">Outgoing Lot</li> <li class="breadcrumb-item active">Outgoing Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li> <li class="breadcrumb-item active">{{ lot.name }}</li>
{% elif lot.is_shared %}
<li class="breadcrumb-item active">Shared with me</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
{% endif %} {% endif %}
</ol> </ol>
</nav> </nav>
@ -39,7 +42,9 @@
<div class="d-flex align-items-center justify-content-between row"> <div class="d-flex align-items-center justify-content-between row">
<div class="col-sm-12 col-md-5"> <div class="col-sm-12 col-md-5">
<h3> <h3>
<a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a> <a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">
{{ lot.name }} {% if lot.is_shared %}<i class="bi bi-arrow-right"></i> {{ lot.owner.email }}{% endif %}
</a>
</h3> </h3>
</div> </div>
@ -54,10 +59,12 @@
Create Incoming Lot Create Incoming Lot
</a> </a>
{% endif %} {% endif %}
{% if not lot.is_shared %}
<a class="text-danger" href="javascript:removeLot()"> <a class="text-danger" href="javascript:removeLot()">
<i class="bi bi-trash"></i> Delete Lot <i class="bi bi-trash"></i> Delete Lot
</a> </a>
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span> <span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -72,7 +79,7 @@
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#devices-list">Devices</button> <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#devices-list">Devices</button>
</li> </li>
{% if lot and not lot.is_temporary %} {% if lot and not lot.is_temporary and not lot.is_shared %}
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button>
</li> </li>
@ -106,6 +113,7 @@
<div class="tab-content pt-1"> <div class="tab-content pt-1">
<div id="devices-list" class="tab-pane fade devices-list active show"> <div id="devices-list" class="tab-pane fade devices-list active show">
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label> <label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
{% if not lot or not lot.is_shared %}
<div class="btn-group dropdown ml-1"> <div class="btn-group dropdown ml-1">
<button id="btnLots" type="button" onclick="processSelectedDevices()" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnLots" type="button" onclick="processSelectedDevices()" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-folder2"></i> <i class="bi bi-folder2"></i>
@ -329,6 +337,24 @@
</li> </li>
</ul> </ul>
</div> </div>
{% endif %}
{% if lot and lot.is_shared %}
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<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>
</ul>
</div>
{% endif %}
<div id="select-devices-info" class="alert alert-info mb-0 mt-3 d-none" role="alert"> <div id="select-devices-info" class="alert alert-info mb-0 mt-3 d-none" role="alert">
If this text is showing is because there are an error If this text is showing is because there are an error
@ -514,7 +540,7 @@
</div> </div>
</div> </div>
{% if lot and not lot.is_temporary %} {% if lot and not lot.is_temporary and not lot.is_shared %}
<div id="trade-documents-list" class="tab-pane fade trade-documents-list"> <div id="trade-documents-list" class="tab-pane fade trade-documents-list">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown=""> <div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{{ url_for('inventory.transfer_document_add', lot_id=lot.id)}}" class="btn btn-primary"> <a href="{{ url_for('inventory.transfer_document_add', lot_id=lot.id)}}" class="btn btn-primary">

View File

@ -11,7 +11,7 @@ from ereuse_devicehub import __version__, messages
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.forms import LoginForm, PasswordForm, SanitizationEntityForm from ereuse_devicehub.forms import LoginForm, PasswordForm, SanitizationEntityForm
from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot, ShareLot
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.utils import is_safe_url from ereuse_devicehub.utils import is_safe_url
@ -89,6 +89,7 @@ class GenericMixin(View):
self.context = { self.context = {
'lots': self.get_lots(), 'lots': self.get_lots(),
'version': __version__, 'version': __version__,
'share_lots': ShareLot.query.filter_by(user_to=g.user),
} }
return self.context return self.context

29
scripts/sharelot.py Normal file
View File

@ -0,0 +1,29 @@
import sys
import uuid
from decouple import config
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.lot.models import Lot, ShareLot
from ereuse_devicehub.resources.user.models import User
def main():
schema = config('DB_SCHEMA')
app = Devicehub(inventory=schema)
app.app_context().push()
email = sys.argv[1]
lot_id = sys.argv[2]
id = uuid.uuid4()
user = User.query.filter_by(email=email).first()
lot = Lot.query.filter_by(id=lot_id).first()
share_lot = ShareLot(id=id, lot=lot, user_to=user)
db.session.add(share_lot)
db.session.commit()
if __name__ == '__main__':
main()

View File

@ -67,6 +67,7 @@ def _app(config: TestConfig) -> Devicehub:
app.register_blueprint(workbench) app.register_blueprint(workbench)
app.config["SQLALCHEMY_RECORD_QUERIES"] = True app.config["SQLALCHEMY_RECORD_QUERIES"] = True
app.config['PROFILE'] = True app.config['PROFILE'] = True
app.config['SCHEMA'] = 'test'
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) # app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
mail = Mail(app) mail = Mail(app)
app.mail = mail app.mail = mail