from datetime import datetime import enum import hashlib import os from flask import current_app from jinja2 import Markup from formity.extensions import db, cache from sqlalchemy import event from sqlalchemy.ext.hybrid import hybrid_property from formity.utils import get_diff class Status(enum.Enum): new = 1 confirmed = 2 allocated = 3 fulfilled = 4 rejected = 5 spam = 6 delegated = 7 intransit = 8 shippingpending = 9 pickuppending = 10 class ShipmentStatus(enum.Enum): unknown = 0 new = 1 booked = 2 collected = 3 inDelivery = 4 returned = 5 delivered = 6 def __ge__(self, other): if self.__class__ is other.__class__: return self.value >= other.value return NotImplemented def __gt__(self, other): if self.__class__ is other.__class__: return self.value > other.value return NotImplemented def __le__(self, other): if self.__class__ is other.__class__: return self.value <= other.value return NotImplemented def __lt__(self, other): if self.__class__ is other.__class__: return self.value < other.value return NotImplemented def get_shipment_status(shipment_info): tracking = shipment_info.get('tracking', {}) if tracking.get('returned'): return ShipmentStatus.returned if tracking.get('delivered'): return ShipmentStatus.delivered if tracking.get('inDelivery'): return ShipmentStatus.inDelivery if tracking.get('collected'): return ShipmentStatus.collected if tracking.get('booked'): return ShipmentStatus.booked if tracking: return ShipmentStatus.new return ShipmentStatus.unknown class PostalCode(db.Model): postalcode = db.Column(db.String, primary_key=True) address = db.Column(db.String, nullable=False) city = db.Column(db.String, nullable=False) voivodeship = db.Column(db.String, nullable=False) county = db.Column(db.String, nullable=False) def __str__(self): return '{}/{}/{}/{}'.format(self.postalcode, self.city, self.county, self.voivodeship) class FaceshieldRequest(db.Model): id = db.Column(db.Integer, primary_key=True) entity_info = db.Column(db.String) full_name = db.Column(db.String) phone_number = db.Column(db.String) email = db.Column(db.String) extra = db.Column(db.String) faceshield_front_required = db.Column(db.Integer, default=0, server_default='0') faceshield_model = db.Column(db.String) faceshield_full_required = db.Column(db.Integer, default=0, server_default='0') created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, onupdate=datetime.utcnow) ua = db.Column(db.String) ip = db.Column(db.String) status = db.Column(db.Enum(Status), default=Status.new, server_default='new', nullable=False) remarks = db.Column(db.String) faceshield_front_delivered = db.Column(db.Integer, default=0, server_default='0') faceshield_full_delivered = db.Column(db.Integer, default=0, server_default='0') adapter_3m_dar_required = db.Column(db.Integer, default=0, server_default='0') adapter_easybreath_dar_required = db.Column(db.Integer, default=0, server_default='0') adapter_rd40_dar_required = db.Column(db.Integer, default=0, server_default='0') adapter_secura_dar_required = db.Column(db.Integer, default=0, server_default='0') adapter_3m_dar_delivered = db.Column(db.Integer, default=0, server_default='0') adapter_easybreath_dar_delivered = db.Column(db.Integer, default=0, server_default='0') adapter_rd40_dar_delivered = db.Column(db.Integer, default=0, server_default='0') adapter_secura_dar_delivered = db.Column(db.Integer, default=0, server_default='0') delivery_method = db.Column(db.String, default='shipping') shipping_name = db.Column(db.String) shipping_street = db.Column(db.String) shipping_postalcode = db.Column(db.String) shipping_city = db.Column(db.String) shipping_latitude = db.Column(db.Float) shipping_longitude = db.Column(db.Float) shipping_provider = db.Column(db.String) shipping_id = db.Column(db.String) postalcode_info = db.relationship(PostalCode, primaryjoin='remote(PostalCode.postalcode) == foreign(FaceshieldRequest.shipping_postalcode)') parent_id = db.Column(db.Integer, db.ForeignKey(id)) parent_shipment = db.relationship('FaceshieldRequest', remote_side=id, backref='children') handling_orga = db.Column(db.String, default='hswaw', server_default='hswaw', nullable=False) handling_orga_info = db.relationship('Orga', primaryjoin='remote(Orga.id) == foreign(FaceshieldRequest.handling_orga)') def __str__(self): return Markup('#{} {} ({})').format(self.id, self.id, self.entity_info, self.status.name) def __repr__(self): return ''.format(self.id, self.status.name) @property def label_count(self): if self.faceshield_full_delivered % 150 == 0 and self.faceshield_full_delivered // 150 > 1: return self.faceshield_full_delivered // 150 return 1 @property def items(self): item_types = ('faceshield_full', 'faceshield_front', 'adapter_3m_dar', 'adapter_easybreath_dar', 'adapter_rd40_dar', 'adapter_secura_dar') return [ { 'type': item, 'required': getattr(self, '{}_required'.format(item)), 'delivered': getattr(self, '{}_delivered'.format(item)), } for item in item_types if getattr(self, '{}_required'.format(item)) or getattr(self, '{}_delivered'.format(item)) ] @hybrid_property def items_total_delivered(self): return self.faceshield_full_delivered + \ self.faceshield_front_delivered + \ self.adapter_3m_dar_delivered + \ self.adapter_easybreath_dar_delivered + \ self.adapter_rd40_dar_delivered + \ self.adapter_secura_dar_delivered @hybrid_property def items_total_required(self): return self.faceshield_full_required + \ self.faceshield_front_required + \ self.adapter_3m_dar_required + \ self.adapter_easybreath_dar_required + \ self.adapter_rd40_dar_required + \ self.adapter_secura_dar_required def fetch_shipping_info(self): if self.shipping_provider == 'kurjerzy': from shipping.kurjerzy import Kurjerzy k = Kurjerzy(current_app) k.authenticate() return [self.cache_shipment_info(k.shipment_info, i) for i in self.shipping_id.split(',')] elif self.shipping_provider == 'xbs': from shipping.dpd import DPD d = DPD(current_app) return [self.cache_shipment_info(d.shipment_info, i) for i in self.shipping_id.split(',')] return None def cache_shipment_info(self, fetch, id): key = 'shipment_info_%d_%s' % (self.id, id) res = cache.get(key) if res is None: try: res = fetch(id) or {'error': 'No shipment found'} except Exception as exc: res = {'error': str(exc)} if res.get('tracking', {}).get('delivered'): cache.set(key, res, timeout=60 * 60 * 24 * 7) else: cache.set(key, res, timeout=60 * 10) return res def refresh_shipping_info(self): for i in self.shipping_id.split(','): cache.delete('shipment_info_%d_%s' % (self.id, i)) @property def shipping_info(self): return self.fetch_shipping_info() @property def shipment_status(self): try: if self.shipping_info: return min([get_shipment_status(s) for s in self.shipping_info], default=ShipmentStatus.unknown) except Exception as exc: print(exc) return ShipmentStatus.unknown def changelog_default_user_id(): from flask_login import current_user return current_user.get_id() if current_user and current_user.get_id() else 'nobody' class RequestChange(db.Model): id = db.Column(db.Integer, primary_key=True) request_id = db.Column(db.Integer, db.ForeignKey(FaceshieldRequest.id)) request = db.relationship(FaceshieldRequest, backref='changelog') user_id = db.Column(db.String, nullable=False, default=changelog_default_user_id) state_before = db.Column(db.JSON) state_after = db.Column(db.JSON) remarks = db.Column(db.String) created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) __mapper_args__ = { "order_by": created } class Orga(db.Model): id = db.Column(db.String, primary_key=True) name = db.Column(db.String) shipping_name = db.Column(db.String) shipping_full_name = db.Column(db.String) shipping_street = db.Column(db.String) shipping_number = db.Column(db.String) shipping_postalcode = db.Column(db.String) shipping_city = db.Column(db.String) shipping_phone_number = db.Column(db.String) shipping_email = db.Column(db.String) class ExternalUser(db.Model): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String, nullable=False) _password = db.Column('password', db.String, nullable=False) remarks = db.Column(db.String) orga = db.Column(db.String, db.ForeignKey(Orga.id)) def _hash(self, salt, password): return hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 213700).hex() @hybrid_property def password(self): return self._password @password.setter def password(self, new_pass): # hack: do not double hash if self._password == new_pass: return salt = hashlib.sha256(os.urandom(16)).hexdigest() self._password = salt + ':' + self._hash(salt, new_pass) def check_password(self, password): salt, h = self.password.split(':') return self._hash(salt, password) == h @event.listens_for(FaceshieldRequest, 'after_update') def on_request_change(mapper, connection, target): try: before, after = get_diff(target) except Exception as exc: # xD before, after = {'diff': 'failed'}, {'diff': 'failed'} if before or after: @event.listens_for(db.session, "after_flush", once=True) def receive_after_flush(session, context): db.session.add(RequestChange( request_id=target.id, state_before=before, state_after=after, ))