diff options
Diffstat (limited to 'servo/models/device.py')
-rw-r--r-- | servo/models/device.py | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/servo/models/device.py b/servo/models/device.py new file mode 100644 index 0000000..ea719a3 --- /dev/null +++ b/servo/models/device.py @@ -0,0 +1,523 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013, First Party Software +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +import re +import gsxws +from os.path import basename +from django_countries import countries +from django.core.validators import RegexValidator + +from django.db import models +from django.conf import settings +from django.core.files import File +from django.core.cache import cache +from django.dispatch import receiver +from django.utils.text import slugify +from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse +from django.db.models.signals import post_save + +from django.contrib.contenttypes.fields import GenericRelation + +from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.managers import CurrentSiteManager + +from servo import defaults +from servo.validators import sn_validator +from servo.models import GsxAccount, Product, DeviceGroup, TaggedItem + + +class Device(models.Model): + """ + The serviceable device + """ + site = models.ForeignKey(Site, editable=False, default=defaults.site_id) + + # @TODO: unique=True would be nice, but complicated... + sn = models.CharField( + blank=True, + default='', + max_length=32, + verbose_name=_("Serial Number"), + validators=[sn_validator] + ) + description = models.CharField( + max_length=128, + default=_("New Device"), + verbose_name=_("description") + ) + brand = models.CharField( + blank=True, + max_length=128, + default=_("Apple"), + verbose_name=_("Brand") + ) + reseller = models.CharField( + blank=True, + default='', + max_length=128, + verbose_name=_("Reseller") + ) + created_at = models.DateTimeField(auto_now_add=True, null=True) + imei = models.CharField( + blank=True, + default='', + max_length=15, + verbose_name=_("IMEI Number") + ) + initial_activation_policy = models.CharField( + default='', + editable=False, + max_length=128, + verbose_name=_("Initial Activation Policy") + ) + applied_activation_policy = models.CharField( + default='', + editable=False, + max_length=128, + verbose_name=_("Applied Activation Policy") + ) + next_tether_policy = models.CharField( + default='', + editable=False, + max_length=128, + verbose_name=_("Next Tether Policy") + ) + unlocked = models.NullBooleanField(default=None, editable=False) + slug = models.SlugField(null=True, editable=False, max_length=128) + PRODUCT_LINES = gsxws.products.models() + LINE_CHOICES = [(k, x['name']) for k, x in PRODUCT_LINES.items()] + product_line = models.CharField( + max_length=16, + default="OTHER", + choices=LINE_CHOICES, + verbose_name=_("Product Line") + ) + products = models.ManyToManyField( + Product, + editable=False, + help_text=_('Products that are compatible with this device instance') + ) + config_code = models.CharField(default='', max_length=8, editable=False) + configuration = models.CharField( + blank=True, + default='', + max_length=256, + verbose_name=_("configuration") + ) + + WARRANTY_CHOICES = ( + ('QP', _("Quality Program")), + ('CS', _("Customer Satisfaction")), + ('ALW', _("Apple Limited Warranty")), + ('APP', _("AppleCare Protection Plan")), + ('CC', _("Custom Bid Contracts")), + ('CBC', _("Custom Bid Contracts")), # sometimes CC, sometimes CBC? + ('WTY', _("3'rd Party Warranty")), + ('OOW', _("Out Of Warranty (No Coverage)")), + ('NA', _("Unknown")), + ) + + warranty_status = models.CharField( + max_length=3, + default="NA", + choices=WARRANTY_CHOICES, + verbose_name=_("Warranty Status") + ) + username = models.CharField( + blank=True, + default='', + max_length=32, + verbose_name=_("username") + ) + password = models.CharField( + blank=True, + default='', + max_length=32, + verbose_name=_("password") + ) + purchased_on = models.DateField( + null=True, + blank=True, + verbose_name=_("Date of Purchase") + ) + purchase_country = models.CharField( + blank=True, + editable=False, + max_length=128, + choices=countries, + default=defaults.country, + verbose_name=_("Purchase Country") + ) + + sla_description = models.TextField(null=True, editable=False) + has_onsite = models.BooleanField( + default=False, + help_text=_('Device is eligible for onsite repairs in GSX') + ) + contract_start_date = models.DateField(null=True, editable=False) + contract_end_date = models.DateField(null=True, editable=False) + onsite_start_date = models.DateField(null=True, editable=False) + onsite_end_date = models.DateField(null=True, editable=False) + + parts_and_labor_covered = models.BooleanField(default=False, editable=False) + + notes = models.TextField(blank=True, default="", verbose_name=_("notes")) + tags = GenericRelation(TaggedItem) + photo = models.ImageField( + null=True, + blank=True, + upload_to="devices", + verbose_name=_("photo") + ) + + image_url = models.URLField( + null=True, + blank=True, + verbose_name=_("Image URL") + ) + manual_url = models.URLField( + null=True, + blank=True, + verbose_name=_("Manual URL") + ) + exploded_view_url = models.URLField( + null=True, + blank=True, + verbose_name=_("Exploded View") + ) + + is_vintage = models.BooleanField( + default=False, + verbose_name='vintage', + help_text=_('Device is considered vintage in GSX') + ) + fmip_active = models.BooleanField(default=False, editable=False) + + objects = CurrentSiteManager() + + def is_apple_device(self): + """ + Checks if this is a valid Apple device SN + """ + valid_sn = gsxws.core.validate(self.sn, 'serialNumber') + valid_imei = gsxws.core.validate(self.imei, 'alternateDeviceId') + return valid_sn or valid_imei + + def get_sn(self): + return self.sn or self.imei + + @property + def has_warranty(self): + return self.warranty_status in ('ALW', 'APP', 'CBC') + + @property + def tag_choices(self): + return TaggedItem.objects.filter(content_type__model="device").distinct("tag") + + def add_tags(self, tags): + tags = [x for x in tags if x != ''] # Filter out empty tags + + if not tags: + return + + content_type = ContentType.objects.get_for_model(Device) + + for t in tags: + tag, created = TaggedItem.objects.get_or_create(content_type=content_type, + object_id=self.pk, + tag=t) + tag.save() + + def get_icon(self): + if re.match('iPad', self.description): + return "ipad" + if re.match('iPhone', self.description): + return "iphone" + if re.match('iPod shuffle', self.description): + return "ipod_shuffle" + if re.match('iPod', self.description): + return "ipod" + if re.match('MacBook', self.description): + return "macbook" + + return "imac" + + def set_wty_status(self, status): + """ + Translates a GSX warranty status description + to our internal representation + """ + if not isinstance(status, basestring): + return + if re.match(r"Apple Limited", status): + self.warranty_status = "ALW" + if re.match(r"AppleCare", status): + self.warranty_status = "APP" + if re.match(r"Customer Satisfaction", status): + self.warranty_status = "CSC" + if re.match(r"Custom Bid", status): + self.warranty_status = "CBC" + if re.match(r"Out Of", status): + self.warranty_status = "OOW" + + def to_dict(self): + result = {'sn': self.sn} + result['description'] = self.description + result['warranty_status'] = self.warranty_status + result['purchased_on'] = self.purchased_on + result['purchase_country'] = self.purchase_country + result['username'] = self.username + result['password'] = self.password + return result + + @classmethod + def from_dict(cls, d): + if d.get('_pk'): + return cls.objects.get(pk=d['_pk']) + + device = Device() + + for k, v in d: + if k.startswith('_'): + continue + setattr(device, k, v) + + return device + + def to_gsx(self): + if len(self.imei): + return gsxws.Product(self.imei) + return gsxws.Product(self.sn) + + @classmethod + def from_gsx(cls, sn, device=None, cached=True): + """ + Initialize new Device with warranty info from GSX + Or update existing one + """ + sn = sn.upper() + cache_key = 'device-%s' % sn + + # Only cache unsaved devices + if cached and device is None: + if cache.get(cache_key): + return cache.get(cache_key) + + arg = gsxws.validate(sn) + + if arg not in ("serialNumber", "alternateDeviceId",): + raise ValueError(_(u"Invalid input for warranty check: %s") % sn) + + product = gsxws.Product(sn) + wty = product.warranty() + model = product.model() + + if device is None: + # serialNumber may sometimes come back empty + serial_number = wty.serialNumber or sn + device = Device(sn=serial_number) + + if device.notes == '': + device.notes = wty.notes or '' + device.notes += wty.csNotes or '' + + device.has_onsite = product.has_onsite + device.is_vintage = product.is_vintage + device.description = product.description + device.fmip_active = product.fmip_is_active + + device.slug = slugify(device.description) + device.configuration = wty.configDescription or '' + device.purchase_country = wty.purchaseCountry or '' + + device.config_code = model.configCode + device.product_line = model.productLine.replace(" ", "") + device.parts_and_labor_covered = product.parts_and_labor_covered + + device.sla_description = wty.slaGroupDescription or '' + device.contract_start_date = wty.contractCoverageStartDate + device.contract_end_date = wty.contractCoverageEndDate + device.onsite_start_date = wty.onsiteStartDate + device.onsite_end_date = wty.onsiteEndDate + + if wty.estimatedPurchaseDate: + device.purchased_on = wty.estimatedPurchaseDate + + device.image_url = wty.imageURL or '' + device.manual_url = wty.manualURL or '' + device.exploded_view_url = wty.explodedViewURL or '' + + if wty.warrantyStatus: + device.set_wty_status(wty.warrantyStatus) + + if product.is_ios: + ad = device.get_activation() + device.imei = ad.imeiNumber or '' + device.unlocked = product.is_unlocked(ad) + device.applied_activation_policy = ad.appliedActivationDetails or '' + device.initial_activation_policy = ad.initialActivationPolicyDetails or '' + device.next_tether_policy = ad.nextTetherPolicyDetails or '' + + cache.set(cache_key, device) + + return device + + def is_mac(self): + """ + Returns True if this is a Mac + """ + p = gsxws.Product(self.sn) + p.description = self.description + return p.is_mac + + def is_ios(self): + """ + Returns True if this is an iOS device + """ + p = gsxws.Product(self.sn) + p.description = self.description + return p.is_ios + + def update_gsx_details(self): + Device.from_gsx(self.sn, self) + self.save() + + def get_image_url(self): + url = 'https://static.servoapp.com/images/products/%s.jpg' % self.slug + return self.image_url or url + + def get_photo(self): + try: + return self.photo.url + except ValueError: + return self.get_image_url() + + def get_fmip_status(self): + """ + Returns the translated FMiP status + """ + return _('Active') if self.fmip_active else _('Inactive') + + def get_coverage_details(self): + details = [] + if self.sla_description: + details.append(_(u'SLA Group: %s') % self.sla_description) + if self.has_onsite: + details.append(_('This unit is eligible for Onsite Service.')) + if self.parts_and_labor_covered: + details.append(_('Parts and Labor are covered.')) + + return details + + @property + def can_create_carryin(self): + if self.description == "Non-Serialized Products": + # Non-serialized products may have more than one repair + return True + + return self.repair_set.filter(completed_at=None).count() < 1 + + def get_accessories(self, order): + return self.accessory_set.filter(order=order).values_list('name', flat=True) + + def get_activation(self): + return gsxws.Product(self.sn).activation() + + def get_diagnostics(self, user): + """ + Fetch GSX iOS or Repair diagnostics based on device type + """ + GsxAccount.default(user) + return self.to_gsx().diagnostics() + + def get_warranty(self): + return gsxws.Product(self.sn).warranty() + + def get_repairs(self): + return gsxws.Product(self.sn).repairs() + + def get_parts(self): + """ + Returns GSX parts for a product with this device's serialNumber + """ + results = {} + cache_key = "%s_parts" % self.sn + + for p in gsxws.Product(self.sn).parts(): + product = Product.from_gsx(p) + results[product.code] = product + + cache.set_many(results) + cache.set(cache_key, results.values()) + + return results.values() + + def import_parts(self): + pass + + def save(self, *args, **kwargs): + if self.sn: + self.sn = self.sn.strip().upper() + + self.description = self.description.strip() + if self.slug is None: + self.slug = slugify(self.description) + + return super(Device, self).save(*args, **kwargs) + + def get_absolute_url(self): + return reverse('devices-view_device', args=[self.product_line, self.slug, self.pk]) + + def get_purchase_country(self): + # Return device's purchase country, can be 2-letter code (from checkin) or + # full country name (from GSX) + from django_countries import countries + + if len(self.purchase_country) > 2: + return self.purchase_country + + return countries.countries.get(self.purchase_country, '') + + def __unicode__(self): + return '%s (%s)' % (self.description, self.sn) + + class Meta: + app_label = "servo" + get_latest_by = "id" + + +@receiver(post_save, sender=Device) +def device_saved(sender, instance, created, **kwargs): + # make sure we have this tag and product category + if created: + DeviceGroup.objects.get_or_create(title=instance.description) + + # Update order descriptions + for o in instance.order_set.all(): + o.description = instance.description + o.save() |