diff --git a/ereuse_devicehub/api/views.py b/ereuse_devicehub/api/views.py index dd06042c..b4ed2032 100644 --- a/ereuse_devicehub/api/views.py +++ b/ereuse_devicehub/api/views.py @@ -75,7 +75,7 @@ class InventoryView(LoginMixin, SnapshotMixin): self.response = jsonify( { 'url': snapshot.device.url.to_text(), - 'dhid': snapshot.device.devicehub_id, + 'dhid': snapshot.device.dhid, 'sid': snapshot.sid, } ) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index b1fdb645..a87442c4 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -110,7 +110,13 @@ class AdvancedSearchForm(FlaskForm): self.search(dhids) def search(self, dhids): - self.devices = Device.query.filter(Device.devicehub_id.in_(dhids)) + query = Device.query.filter(Device.owner_id == g.user.id) + self.devices = query.join(Device.placeholder).filter( + or_( + Device.devicehub_id.in_(dhids), + Placeholder.phid.in_(dhids), + ) + ) class FilterForm(FlaskForm): @@ -137,11 +143,11 @@ class FilterForm(FlaskForm): self.lot = self.lots.filter(Lot.id == self.lot_id).one() device_ids = (d.id for d in self.lot.devices) self.devices = Device.query.filter(Device.id.in_(device_ids)).filter( - Device.binding == None + Device.binding == None # noqa: E711 ) else: self.devices = Device.query.filter(Device.owner_id == g.user.id).filter( - Device.binding == None + Device.binding == None # noqa: E711 ) if self.only_unassigned: self.devices = self.devices.filter_by(lots=None) @@ -364,18 +370,6 @@ class NewDeviceForm(FlaskForm): if not self.generation.data: self.generation.data = 1 - if not self.weight.data: - self.weight.data = 0.1 - - if not self.height.data: - self.height.data = 0.1 - - if not self.width.data: - self.width.data = 0.1 - - if not self.depth.data: - self.depth.data = 0.1 - def reset_from_obj(self): if not self._obj: return @@ -449,24 +443,28 @@ class NewDeviceForm(FlaskForm): error = ["Not a correct value"] is_valid = super().validate(extra_validators) - if self.generation.data < 1: + if self.generation.data and self.generation.data < 1: self.generation.errors = error is_valid = False - if self.weight.data < 0.1: - self.weight.errors = error + if self.weight.data and not (0.1 <= self.weight.data <= 5): + txt = ["Supported values between 0.1 and 5"] + self.weight.errors = txt is_valid = False - if self.height.data < 0.1: - self.height.errors = error + if self.height.data and not (0.1 <= self.height.data <= 5): + txt = ["Supported values between 0.1 and 5"] + self.height.errors = txt is_valid = False - if self.width.data < 0.1: - self.width.errors = error + if self.width.data and not (0.1 <= self.width.data <= 5): + txt = ["Supported values between 0.1 and 5"] + self.width.errors = txt is_valid = False - if self.depth.data < 0.1: - self.depth.errors = error + if self.depth.data and not (0.1 <= self.depth.data <= 5): + txt = ["Supported values between 0.1 and 5"] + self.depth.errors = txt is_valid = False if self.imei.data and self.amount.data == 1: @@ -655,19 +653,30 @@ class NewDeviceForm(FlaskForm): class TagDeviceForm(FlaskForm): - tag = SelectField('Tag', choices=[]) - device = StringField('Device', [validators.Optional()]) + tag = SelectField( + 'Tag', + choices=[], + render_kw={ + 'class': 'form-control selectpicker', + 'data-live-search': 'true', + }, + ) def __init__(self, *args, **kwargs): self.delete = kwargs.pop('delete', None) - self.device_id = kwargs.pop('device', None) + self.dhid = kwargs.pop('dhid', None) + self._device = ( + Device.query.filter(Device.devicehub_id == self.dhid) + .filter(Device.owner_id == g.user.id) + .one() + ) super().__init__(*args, **kwargs) if self.delete: tags = ( Tag.query.filter(Tag.owner_id == g.user.id) - .filter_by(device_id=self.device_id) + .filter_by(device_id=self._device.id) .order_by(Tag.id) ) else: @@ -695,20 +704,6 @@ class TagDeviceForm(FlaskForm): self.tag.errors = [("This tag is actualy in use.")] return False - if self.device.data: - try: - self.device.data = int(self.device.data.split(',')[-1]) - except: # noqa: E722 - self.device.data = None - - if self.device_id or self.device.data: - self.device_id = self.device_id or self.device.data - self._device = ( - Device.query.filter(Device.id == self.device_id) - .filter(Device.owner_id == g.user.id) - .one() - ) - return True def save(self): @@ -1667,8 +1662,8 @@ class BindingForm(FlaskForm): self.phid.errors = [txt] return False - if self.device.is_abstract() != 'Abstract': - txt = "This is not a abstract device." + if self.device.is_abstract() not in ['Abstract', 'Real']: + txt = "This is not a Abstract or Real device." self.phid.errors = [txt] return False @@ -1682,7 +1677,7 @@ class BindingForm(FlaskForm): self.phid.errors = [txt] return False - if self.placeholder.binding: + if self.placeholder.status not in ['Abstract', 'Real']: txt = "This placeholder have a binding with other device. " txt += "Before you need to do an unbinding with this other device." self.phid.errors = [txt] diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index a797a705..8b65e37a 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -80,7 +80,6 @@ class DeviceListMixin(GenericMixin): self.context.update( { 'devices': devices, - 'form_tag_device': TagDeviceForm(), 'form_new_action': form_new_action, 'form_new_allocate': AllocateForm(lot=lot_id), 'form_new_datawipe': DataWipeForm(lot=lot_id), @@ -148,27 +147,51 @@ class DeviceDetailView(GenericMixin): .one() ) - form_binding = BindingForm(device=device) - + form_tags = TagDeviceForm(dhid=id) self.context.update( { 'device': device, 'placeholder': device.binding or device.placeholder, 'page_title': 'Device {}'.format(device.devicehub_id), + 'form_tag_device': form_tags, + } + ) + + return flask.render_template(self.template_name, **self.context) + + +class BindingSearchView(GenericMixin): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/binding_search.html' + + def dispatch_request(self, dhid): + self.get_context() + device = ( + Device.query.filter(Device.owner_id == current_user.id) + .filter(Device.devicehub_id == dhid) + .one() + ) + + form_binding = BindingForm(device=device) + + self.context.update( + { + 'page_title': 'Search a Device for to do a binding from {}'.format( + device.devicehub_id + ), 'form_binding': form_binding, - 'active_binding': False, + 'device': device, } ) if form_binding.validate_on_submit(): next_url = url_for( 'inventory.binding', - dhid=form_binding.device.devicehub_id, + dhid=dhid, phid=form_binding.placeholder.phid, ) return flask.redirect(next_url) - elif form_binding.phid.data: - self.context['active_binding'] = True return flask.render_template(self.template_name, **self.context) @@ -179,71 +202,113 @@ class BindingView(GenericMixin): template_name = 'inventory/binding.html' def dispatch_request(self, dhid, phid): + self.phid = phid + self.dhid = dhid + self.next_url = url_for('inventory.device_details', id=dhid) self.get_context() - device = ( - Device.query.filter(Device.owner_id == g.user.id) - .filter(Device.devicehub_id == dhid) - .one() - ) - placeholder = ( - Placeholder.query.filter(Placeholder.owner_id == g.user.id) - .filter(Placeholder.phid == phid) - .one() - ) - - if device.is_abstract() != 'Abstract': - next_url = url_for('inventory.device_details', id=dhid) - messages.error('Device "{}" not is a Abstract device!'.format(dhid)) - return flask.redirect(next_url) - - if device.placeholder: - device = device.placeholder.binding - dhid = device.devicehub_id + self.get_objects() + if self.check_errors(): + return flask.redirect(self.next_url) if request.method == 'POST': - old_placeholder = device.binding - old_device_placeholder = old_placeholder.device - - if old_placeholder.is_abstract: - for plog in PlaceholdersLog.query.filter_by( - placeholder_id=old_placeholder.id - ): - db.session.delete(plog) - - for ac in old_device_placeholder.actions: - ac.devices.add(placeholder.device) - ac.devices.remove(old_device_placeholder) - for act in ac.actions_device: - if act.device == old_device_placeholder: - db.session.delete(act) - - for tag in list(old_device_placeholder.tags): - tag.device = placeholder.device - - db.session.delete(old_device_placeholder) - - device.binding = placeholder - db.session.commit() - next_url = url_for('inventory.device_details', id=dhid) - messages.success( - 'Device "{}" bind successfully with {}!'.format(dhid, phid) - ) - return flask.redirect(next_url) + return self.post() self.context.update( { - 'device': device.binding.device, - 'placeholder': placeholder, + 'new_placeholder': self.new_placeholder, + 'old_placeholder': self.old_placeholder, 'page_title': 'Binding confirm', - 'actions': list(device.binding.device.actions) - + list(placeholder.device.actions), - 'tags': list(device.binding.device.tags) - + list(placeholder.device.tags), + 'actions': list(self.old_device.actions) + + list(self.new_device.actions), + 'tags': list(self.old_device.tags) + list(self.new_device.tags), + 'dhid': self.dhid, } ) return flask.render_template(self.template_name, **self.context) + def check_errors(self): + if not self.new_placeholder: + messages.error('Device Phid: "{}" not exist!'.format(self.phid)) + return True + + if self.old_device.placeholder.status != 'Abstract': + messages.error( + 'Device Dhid: "{}" is not a Abstract device!'.format(self.dhid) + ) + return True + + if self.new_placeholder.status == 'Twin': + messages.error('Device Phid: "{}" is a Twin device!'.format(self.phid)) + return True + + if self.new_placeholder.status == self.old_placeholder.status: + txt = 'Device Phid: "{}" and device Dhid: "{}" have the same status, "{}"!'.format( + self.phid, self.dhid, self.new_placeholder.status + ) + messages.error(txt) + return True + + def get_objects(self): + self.old_device = ( + Device.query.filter(Device.owner_id == g.user.id) + .filter(Device.devicehub_id == self.dhid) + .one() + ) + self.new_placeholder = ( + Placeholder.query.filter(Placeholder.owner_id == g.user.id) + .filter(Placeholder.phid == self.phid) + .first() + ) + + if not self.new_placeholder: + return + + if self.old_device.placeholder.status == 'Abstract': + self.new_device = self.new_placeholder.device + self.old_placeholder = self.old_device.placeholder + elif self.old_device.placeholder.status == 'Real': + self.new_device = self.old_device + self.old_placeholder = self.new_placeholder + self.old_device = self.old_placeholder.device + self.new_placeholder = self.new_device.placeholder + + self.abstract_device = self.old_placeholder.binding + self.real_dhid = self.new_device.devicehub_id + self.real_phid = self.new_placeholder.phid + self.abstract_dhid = self.old_device.devicehub_id + self.abstract_phid = self.old_placeholder.phid + + def post(self): + for plog in PlaceholdersLog.query.filter_by( + placeholder_id=self.old_placeholder.id + ): + db.session.delete(plog) + + for ac in self.old_device.actions: + ac.devices.add(self.new_device) + ac.devices.remove(self.old_device) + for act in ac.actions_device: + if act.device == self.old_device: + db.session.delete(act) + + for tag in list(self.old_device.tags): + tag.device = self.new_device + + db.session.delete(self.old_device) + self.abstract_device.binding = self.new_placeholder + db.session.commit() + + next_url = url_for('inventory.device_details', id=self.real_dhid) + txt = 'Device real with PHID: {} and DHID: {} bind successfully with ' + txt += 'device abstract PHID: {} DHID: {}.' + messages.success( + txt.format( + self.real_phid, self.real_dhid, self.abstract_phid, self.abstract_dhid + ) + ) + return flask.redirect(next_url) + class UnBindingView(GenericMixin): methods = ['GET', 'POST'] @@ -256,31 +321,33 @@ class UnBindingView(GenericMixin): .filter(Placeholder.phid == phid) .one() ) - if not placeholder.binding: + if not placeholder.binding or placeholder.status != 'Twin': next_url = url_for( 'inventory.device_details', id=placeholder.device.devicehub_id ) return flask.redirect(next_url) - device = placeholder.binding - - if device.is_abstract() != 'Twin': - dhid = device.devicehub_id + if placeholder.status != 'Twin': + dhid = placeholder.device.devicehub_id next_url = url_for('inventory.device_details', id=dhid) - messages.error('Device "{}" not is a Twin device!'.format(dhid)) + messages.error('Device Dhid: "{}" not is a Twin device!'.format(dhid)) return flask.redirect(next_url) self.get_context() if request.method == 'POST': - new_device = self.clone_device(device) - next_url = url_for('inventory.device_details', id=new_device.devicehub_id) - messages.success('Device "{}" unbind successfully!'.format(phid)) + dhid = placeholder.device.devicehub_id + self.clone_device(placeholder.binding) + next_url = url_for('inventory.device_details', id=dhid) + messages.success( + 'Device with PHID:"{}" and DHID: {} unbind successfully!'.format( + phid, dhid + ) + ) return flask.redirect(next_url) self.context.update( { - 'device': device, 'placeholder': placeholder, 'page_title': 'Unbinding confirm', } @@ -476,12 +543,15 @@ class TagLinkDeviceView(View): methods = ['POST'] decorators = [login_required] - def dispatch_request(self): - form = TagDeviceForm() + def dispatch_request(self, dhid): + form = TagDeviceForm(dhid=dhid) if form.validate_on_submit(): + tag = form.tag.data form.save() - return flask.redirect(request.referrer) + next_url = url_for('inventory.device_details', id=dhid) + messages.success('Tag {} was linked successfully!'.format(tag)) + return flask.redirect(next_url) class TagUnlinkDeviceView(GenericMixin): @@ -489,19 +559,20 @@ class TagUnlinkDeviceView(GenericMixin): decorators = [login_required] template_name = 'inventory/tag_unlink_device.html' - def dispatch_request(self, id): + def dispatch_request(self, dhid): self.get_context() - form = TagDeviceForm(delete=True, device=id) + form = TagDeviceForm(delete=True, dhid=dhid) if form.validate_on_submit(): form.remove() - next_url = url_for('inventory.devicelist') + next_url = url_for('inventory.device_details', id=dhid) + messages.success('Tag {} was unlinked successfully!'.format(form.tag.data)) return flask.redirect(next_url) self.context.update( { 'form': form, - 'referrer': request.referrer, + 'dhid': dhid, } ) @@ -1145,10 +1216,11 @@ devices.add_url_rule( '/device/edit//', view_func=DeviceEditView.as_view('device_edit') ) devices.add_url_rule( - '/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add') + '/tag/devices//add/', + view_func=TagLinkDeviceView.as_view('tag_devices_add'), ) devices.add_url_rule( - '/tag/devices//del/', + '/tag/devices//del/', view_func=TagUnlinkDeviceView.as_view('tag_devices_del'), ) devices.add_url_rule( @@ -1192,3 +1264,7 @@ devices.add_url_rule( devices.add_url_rule( '/unbinding//', view_func=UnBindingView.as_view('unbinding') ) +devices.add_url_rule( + '/device//binding/', + view_func=BindingSearchView.as_view('binding_search'), +) diff --git a/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_create_placeholders.py b/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_create_placeholders.py index 6abff7a8..87a7154b 100644 --- a/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_create_placeholders.py +++ b/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_create_placeholders.py @@ -63,6 +63,9 @@ def clone_device(device): if device.type == "Battery": device.size + + old_devicehub_id = device.devicehub_id + dict_device = copy.copy(device.__dict__) dict_device.pop('_sa_instance_state') dict_device.pop('id', None) @@ -73,6 +76,8 @@ def clone_device(device): dict_device.pop('tags', None) dict_device.pop('system_uuid', None) new_device = device.__class__(**dict_device) + new_device.devicehub_id = old_devicehub_id + device.devicehub_id = None db.session.add(new_device) if hasattr(device, 'components'): diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index a50d1593..f37ef586 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -330,7 +330,7 @@ class Device(Thing): @property def url(self) -> urlutils.URL: """The URL where to GET this device.""" - return urlutils.URL(url_for_resource(Device, item_id=self.devicehub_id)) + return urlutils.URL(url_for_resource(Device, item_id=self.dhid)) @property def rate(self): @@ -615,6 +615,14 @@ class Device(Thing): model = self.model or '' return f'{type} {manufacturer} {model}' + @property + def dhid(self): + if self.placeholder: + return self.placeholder.device.devicehub_id + if self.binding: + return self.binding.device.devicehub_id + return self.devicehub_id + @declared_attr def __mapper_args__(cls): """Defines inheritance. @@ -924,6 +932,25 @@ class Placeholder(Thing): ) owner = db.relationship(User, primaryjoin=owner_id == User.id) + @property + def actions(self): + actions = list(self.device.actions) or [] + + if self.binding: + actions.extend(list(self.binding.actions)) + + actions = sorted(actions, key=lambda x: x.created) + actions.reverse() + return actions + + @property + def status(self): + if self.is_abstract: + return 'Abstract' + if self.binding: + return 'Twin' + return 'Real' + class Computer(Device): """A chassis with components inside that can be processed @@ -1022,10 +1049,14 @@ class Computer(Device): """Returns the privacy of all ``DataStorage`` components when it is not None. """ + components = self.components + if self.placeholder and self.placeholder.binding: + components = self.placeholder.binding.components + return set( privacy for privacy in ( - hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage) + hdd.privacy for hdd in components if isinstance(hdd, DataStorage) ) if privacy ) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 2cc8e65d..2b9530db 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -114,7 +114,7 @@ class Device(Thing): sku = SanitizedStr(description=m.Device.sku.comment) image = URL(description=m.Device.image.comment) allocated = Boolean(description=m.Device.allocated.comment) - devicehub_id = SanitizedStr( + dhid = SanitizedStr( data_key='devicehubID', description=m.Device.devicehub_id.comment ) diff --git a/ereuse_devicehub/resources/device/templates/devices/layout.html b/ereuse_devicehub/resources/device/templates/devices/layout.html index e4474426..f513d0b9 100644 --- a/ereuse_devicehub/resources/device/templates/devices/layout.html +++ b/ereuse_devicehub/resources/device/templates/devices/layout.html @@ -41,8 +41,8 @@
@@ -52,7 +52,7 @@
    {% for key, value in device.public_properties.items() %} -
  • {{ key }}: {{ value }}
  • +
  • {{ key }}: {{ value or '' }}
  • {% endfor %}
{% if isinstance(device, d.Computer) %} @@ -140,17 +140,6 @@ {% endif %} - {% if device.rate %} - - - Total rate - - - {{ device.rate.rating_range }} - ({{ device.rate.rating }}) - - - {% endif %}
@@ -222,6 +211,181 @@ {% endif %}
+ {% if abstract %} +
+ +
+
+
+
+
+
    + {% for key, value in abstract.public_properties.items() %} +
  • {{ key }}: {{ value or '' }}
  • + {% endfor %} +
+ {% if isinstance(abstract, d.Computer) %} +
+ + + + + + + + + {% if abstract.processor_model %} + + + + + {% endif %} + {% if abstract.ram_size %} + + + + + {% endif %} + {% if abstract.data_storage_size %} + + + + + {% endif %} + {% if abstract.graphic_card_model %} + + + + + {% endif %} + {% if abstract.network_speeds %} + + + + + {% endif %} + +
Range
+ CPU – {{ abstract.processor_model }} + + Processor Rate = {% if abstract.rate %} + {{ abstract.rate.processor_range }} + ({{ abstract.rate.processor }}) + {% endif %} +
+ RAM – {{ abstract.ram_size // 1000 }} GB + {{ macros.component_type(abstract.components, 'RamModule') }} + + RAM Rate = {% if abstract.rate %} + {{ abstract.rate.ram_range }} + ({{ abstract.rate.ram }}) + {% endif %} +
+ Data Storage – {{ abstract.data_storage_size // 1000 }} GB + {{ macros.component_type(abstract.components, 'SolidStateDrive') }} + {{ macros.component_type(abstract.components, 'HardDrive') }} + + Data Storage Rate = {% if abstract.rate %} + {{ abstract.rate.data_storage_range }} + ({{ abstract.rate.data_storage }}) + {% endif %} +
+ Graphics – {{ abstract.graphic_card_model }} + {{ macros.component_type(abstract.components, 'GraphicCard') }} +
+ Network – + {% if abstract.network_speeds[0] %} + Ethernet + {% if abstract.network_speeds[0] != None %} + max. {{ abstract.network_speeds[0] }} Mbps + {% endif %} + {% endif %} + {% if abstract.network_speeds[0] and abstract.network_speeds[1] %} + + + {% endif %} + {% if abstract.network_speeds[1] %} + WiFi + {% if abstract.network_speeds[1] != None %} + max. {{ abstract.network_speeds[1] }} Mbps + {% endif %} + {% endif %} + {{ macros.component_type(abstract.components, 'NetworkAdapter') }} +
+
+

Actual Status

+
    +
  1. + + Lifecycle Status + + — + {% if abstract.status %} + {{ abstract.status.type }} + {% endif %} +
  2. +
  3. + + Allocate Status + + — + {% if abstract.allocated_status %} + {{ abstract.allocated_status.type }} + {% endif %} +
  4. +
  5. + + Physical Status + + — + {% if abstract.physical_status %} + {{ abstract.physical_status.type }} + {% endif %} +
  6. +
+ +

Public traceability log of the abstract

+
+ Latest one. +
+
    + {% for action in abstract.public_actions %} +
  1. + + {{ abstract.is_status(action) }} + {% if not abstract.is_status(action) %} + {{ action.type }} + {% endif %} + + — + {% if abstract.is_status(action) %} + {{ action }} {{ action.type }} + {% else %} + {{ action }} + {% endif %} +
    +
    + + {{ action._date_str }} + +
    + {% if action.certificate %} + See the certificate + {% endif %} +
  2. + {% endfor %} +
+
+ Oldest one. +
+ {% endif %} +
+
+ {% endif %}