class Acknowledgement(models.Model): burn = models.ForeignKey('PrescribedBurn', related_name='acknowledgements', on_delete=models.PROTECT) user = models.ForeignKey(User, help_text="User", null=True, blank=True, on_delete=models.PROTECT) acknow_type = models.CharField(max_length=64, null=True, blank=True) acknow_date = models.DateTimeField(auto_now_add=True, null=True, blank=True) fmt = "%d/%m/%Y %H:%M" @property def record(self): username = '******'.format(self.user.first_name[0], self.user.last_name) return "{} {}".format( username, self.acknow_date.astimezone(tz.tzlocal()).strftime(self.fmt)) def remove(self): self.delete() def __str__(self): return "{} - {} - {}".format(self.burn, self.acknow_type, self.record)
class Token(models.Model): link = models.ForeignKey(ApplicationLink) user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s_user", help_text="User token authenticates as") url = models.TextField(help_text="Suburl this token is restricted to, " "relative e.g. (/my/single/service/entrypoint)", default="/") secret = models.CharField(max_length=320, help_text="Token Secret", unique=True) modified = models.DateTimeField(default=timezone.now, editable=False) timeout = models.IntegerField(default=600, help_text="Timeout token in " "seconds, 0 means never times out") class Meta: app_label = 'swingers' def save(self, *args, **kwargs): try: revision.unregister(self.__class__) except: pass super(Token, self).save(*args, **kwargs) def natural_key(self): return (self.secret, ) def get_by_natural_key(self, secret): return self.get(secret=secret) def __str__(self): return "{0} - {1}:{2}@{3}".format(self.pk, self.user, self.secret, self.link.client_name)[:320]
class DocumentAbstract(Audit, ActiveModel): ''' Generic class for supporting documents. ''' content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') uploaded_file = models.FileField( # max_length is maximum full path and filename length: max_length=255, upload_to=content_filename) description = models.TextField( blank=True, null=True, help_text='Name and/or description of the supporting document.') class Meta: abstract = True def __unicode__(self): return unicode(self.pk) @property def uploaded_file_name(self): """ Return the file name of the uploaded file, minus the server file path. """ try: return self.uploaded_file.name.rsplit('/', 1)[-1] except: # If the file has been deleted/is missing, return a warning. return '<missing_file>' @property def uploaded_file_ext(self): ''' Return the file extension of the uploaded file. ''' try: ext = os.path.splitext(self.uploaded_file.name)[1] return ext.replace('.', '').upper() except: # If the file has been deleted/is missing, return an empty string. return '' @property def filesize_str(self): ''' Return the filesize as a nicely human-readable string. ''' try: num = self.uploaded_file.size for x in ['bytes', 'KB', 'MB', 'GB']: if num < 1024.0: return '%3.1f%s' % (num, x) num /= 1024.0 except: # If the file has been deleted/is missing, return an empty string. return ''
class OperationalOverview(Audit): prescription = models.ForeignKey(Prescription, on_delete=models.PROTECT) overview = models.TextField(blank=True, null=True) def __str__(self): return self.overview class Meta: get_latest_by = 'id'
class DocumentTag(models.Model): name = models.CharField(verbose_name="Document Tag", max_length=200) category = models.ForeignKey(DocumentCategory, on_delete=models.PROTECT) objects = CategoryManager() class Meta: ordering = ['name'] def __str__(self): return self.name
class BurnState(models.Model): prescription = models.ForeignKey(Prescription, related_name='burnstate', on_delete=models.PROTECT) user = models.ForeignKey(User, help_text="User", on_delete=models.PROTECT) review_type = models.CharField(max_length=64) review_date = models.DateTimeField(auto_now_add=True) fmt = "%d/%m/%Y %H:%M:%S" @property def record(self): username = '******'.format(self.user.first_name[0], self.user.last_name) return "{} {}".format( username, self.review_date.astimezone(tz.tzlocal()).strftime(self.fmt)) def __str__(self): return "{} - {} - {}".format(self.prescription, self.review_type, self.record)
class ExclusionArea(Audit): prescription = models.ForeignKey( Prescription, help_text="Prescription this exclusion area belongs to.", on_delete=models.PROTECT) description = models.TextField() location = models.TextField() detail = models.TextField(verbose_name="How will fire be excluded?") _required_fields = ('location', 'description', 'detail') def __str__(self): return "{0} - {1}".format(self.location, self.description)
class ProposedAction(Audit): prescription = models.ForeignKey(Prescription, on_delete=models.PROTECT) observations = models.TextField(blank=True, verbose_name='Observations Identified') action = models.TextField(blank=True, verbose_name='Proposed Action') _required_fields = ('observations', 'action') def __str__(self): return self.action class Meta: verbose_name = 'Lesson Learned' verbose_name_plural = "Lessons Learned"
class PostBurnChecklist(Audit): prescription = models.ForeignKey(Prescription, blank=True, null=True, on_delete=models.PROTECT) action = models.CharField(max_length=320) relevant = models.BooleanField(default=False) completed_on = models.DateField(default=timezone.now, blank=True, null=True) completed_by = models.TextField(verbose_name="Action completed by (name)", blank=True, null=True) _required_fields = ('completed_on', 'completed_by') def __str__(self): return self.action def clean_completed_on(self): if self.completed_on is not None: # The following line was causing validation failure before 0800. # Jira ref: PBS-1454 #if self.completed_on > timezone.now().date(): if self.completed_on > datetime.now().date(): raise ValidationError("This action could not be completed " "in the future.") if (self.prescription.approval_status_modified is None or self.completed_on < self.prescription.approval_status_modified.date()): raise ValidationError("This action could not be completed " "before its prescription was approved.") def clean(self, *args, **kwargs): super(PostBurnChecklist, self).clean(*args, **kwargs) if self.completed_on is not None and not self.completed_by: raise ValidationError("Please specify who completed this action.") if self.completed_by and self.completed_on is None: raise ValidationError("Please specify when was this action " "completed.") class Meta: ordering = ["pk"] verbose_name = "post burn checklist item" verbose_name_plural = "post burn checklist"
class Way(Audit): prescription = models.ForeignKey( Prescription, help_text="Prescription this belongs to.", on_delete=models.PROTECT) name = models.CharField(max_length=300) signs_installed = models.DateField( verbose_name="Signs Installed", null=True, blank=True) signs_removed = models.DateField( verbose_name="Signs Removed", null=True, blank=True) def __str__(self): return self.name def clean_signs_removed(self): if ((self.signs_removed and self.signs_installed and self.signs_removed < self.signs_installed)): raise ValidationError('Signs cannot be removed earlier than the ' 'install date.')
class SignInspection(Audit): way = models.ForeignKey( Way, verbose_name="Road/Track/Trail Name", on_delete=models.PROTECT) inspected = models.DateTimeField( default=timezone.now, verbose_name="Date Inspected") comments = models.TextField() inspector = models.TextField(verbose_name="Inspecting Officer") def __str__(self): return "%s (%s)" % (self.way.name, date(self.inspected)) @property def prescription(self): return self.way.prescription class Meta: ordering = ['id'] verbose_name = "Sign Inspection" verbose_name_plural = "Sign Inspections"
class RoadSegment(Way): road_type = models.TextField( verbose_name="Road Type", blank=True) traffic_considerations = models.TextField( blank=True, verbose_name="Special Traffic Considerations") traffic_diagram = models.ForeignKey( TrafficControlDiagram, null=True, blank=True, verbose_name="Select Traffic Control Diagram", on_delete=models.PROTECT) _required_fields = ('name', 'road_type',) def __str__(self): return self.name class Meta: ordering = ['id'] verbose_name = "Road" verbose_name_plural = "Roads"
def test_imports(self): from swingers import models # check all these are exposed via models.* models.Model models.ForeignKey(ActiveDuck) models.Manager() models.ActiveModel models.ActiveModelManager() models.Audit self.assertEqual(models.GIS_ENABLED, getattr(settings, 'GIS_ENABLED', False)) if models.GIS_ENABLED: self.assertTrue(issubclass(models.Manager, GeoManager)) self.assertTrue(issubclass(models.ActiveModel, models.base.ActiveGeoModel)) self.assertTrue(issubclass(models.ActiveModelManager, models.managers.ActiveGeoModelManager)) else: self.assertTrue(issubclass(models.Manager, Manager)) self.assertFalse(issubclass(models.ActiveModel, models.base.ActiveGeoModel)) self.assertFalse(issubclass(models.ActiveModelManager, models.managers.ActiveGeoModelManager)) # check the GEO stuff validates models.managers.ActiveGeoModelManager() models.managers.ActiveGeoQuerySet(ActiveDuck) models.base.ActiveGeoModel # check the normal stuff validates models.managers.ActiveModelManager() models.managers.ActiveQuerySet(ActiveDuck) models.base.ActiveModel
class AreaAchievement(Audit): prescription = models.ForeignKey(Prescription, on_delete=models.PROTECT) #Jira issue PBS-1407 ignition = models.DateField(verbose_name="Ignition Date", ) # default=lambda: timezone.now().date()) ignition_types = models.ManyToManyField(IgnitionType) area_treated = models.DecimalField( verbose_name="Area where treatment is complete (ha)", validators=[MinValueValidator(0)], default=0, decimal_places=1, max_digits=12) area_estimate = models.DecimalField(verbose_name="Area treated today (ha)", validators=[MinValueValidator(0)], default=0, decimal_places=1, max_digits=12) edging_length = models.DecimalField( verbose_name="Length of Successful Edging (kms)", validators=[MinValueValidator(0)], default=0, decimal_places=1, max_digits=12) edging_depth_estimate = models.DecimalField( verbose_name="Estimated Depth of Edging (m)", validators=[MinValueValidator(0)], default=0, decimal_places=1, max_digits=12) dpaw_fire_no = models.CharField(verbose_name="DPaW Fire Number", max_length=64, blank=True) dfes_fire_no = models.CharField(verbose_name="DFES Fire Number", max_length=64, blank=True) date_escaped = models.DateField(verbose_name="Date of Escape", null=True, blank=True) _required_fields = ('ignition', 'ignition_types', 'area_treated', 'area_estimate') class Meta: ordering = ['-ignition'] get_latest_by = 'ignition' verbose_name = "Day of Burn Achievement" verbose_name_plural = "Day of Burn Achievements" def __str__(self): return "%s %d ha %d kms" % (self.ignition, self.area_estimate, self.edging_length) def clean_ignition(self): if self.ignition and self.ignition > timezone.now().date(): raise ValidationError("Ignition date cannot be in the future.") def clean_date_escaped(self): if self.date_escaped: if self.date_escaped > timezone.now().date(): raise ValidationError("Date of escape cannot be in the " "future.") if not self.dpaw_fire_no and not self.dfes_fire_no: raise ValidationError("If there is a date of escape, there " "must also be a DPaW or DFES fire no.") def save(self, **kwargs): super(AreaAchievement, self).save(**kwargs) self.prescription.save() def delete(self, **kwargs): super(AreaAchievement, self).delete(**kwargs) obj = self.prescription if obj.areaachievement_set.all().count() == 0: obj.ignition_completed_date = None obj.save()
class Document(Audit): prescription = models.ForeignKey( Prescription, null=True, help_text="Prescription that this document belongs to", on_delete=models.PROTECT) category = models.ForeignKey(DocumentCategory, related_name="documents", on_delete=models.PROTECT) tag = ChainedForeignKey( DocumentTag, chained_field="category", chained_model_field="category", show_all=False, auto_choose=True, verbose_name="Descriptor", on_delete=models.PROTECT) custom_tag = models.CharField( max_length=64, blank=True, verbose_name="Custom Descriptor") document = ContentTypeRestrictedFileField( upload_to=content_file_name, max_length=200, content_types=['application/pdf', 'image/tiff', 'image/tif', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'application/zip', 'application/x-zip-compressed'], help_text='Acceptable file types: pdf, tiff, jpg, gif, png, zip') document_created = models.DateTimeField( verbose_name="Date Document Created", default=timezone.now, editable=True, null=True, blank=True) document_archived = models.BooleanField(default=False, verbose_name="Archived Document") objects = TagManager() def save(self, *args, **kwargs): super(Document, self).save(*args, **kwargs) # confirm that file is written to filesystem, if not remove the record if not self.exists: fname = self.document.name Document.objects.get(id=self.id).delete() raise Exception('ERROR: File not created on filesystem {}'.format(fname)) return @property def descriptor(self): if self.custom_tag: return "Other ({0})".format(self.custom_tag) else: return self.tag.name @property def dimensions(self): return get_dimensions(self.document.path) @property def filename(self): try: return os.path.basename(self.document.path) except: return None @property def exists(self): """ Check if file exists on the file system """ try: return os.path.exists(self.document.file.name) except: return False @property def is_zipped(self): return self.filename.endswith('.zip') class Meta: ordering = ['tag', 'document'] permissions = ( ("archive_document", "Can archive documents") ) def __str__(self): return "{0} - {1}".format(self.prescription, self.document.name)
class LightingSequence(Audit): prescription = models.ForeignKey( Prescription, help_text="Prescription this lighting sequence belongs to.", on_delete=models.PROTECT) seqno = models.PositiveSmallIntegerField( verbose_name="Lighting Sequence Number", choices=Prescription.INT_CHOICES) cellname = models.TextField(verbose_name="Cell Name") strategies = models.TextField( help_text="Textual description of strategies for this sequence") wind_min = models.PositiveIntegerField( verbose_name="Min Wind Speed (km/h)", validators=[MaxValueValidator(200)], blank=True) wind_max = models.PositiveIntegerField( verbose_name="Max Wind Speed (km/h)", validators=[MinValueValidator(0), MaxValueValidator(200)], blank=True) wind_dir = models.TextField(verbose_name="Wind Direction") fuel_description = models.TextField( help_text="Textual description of the fuel for this sequence") fuel_age = models.PositiveSmallIntegerField(verbose_name="Fuel Age", help_text="Fuel Age in years", null=True, blank=True) fuel_age_unknown = models.BooleanField(verbose_name="Fuel Age Unknown?", default=False) ignition_types = models.ManyToManyField( IgnitionType, verbose_name="Planned Core Ignition Type") ffdi_min = models.PositiveIntegerField(verbose_name="FFDI Min", blank=True) ffdi_max = models.PositiveIntegerField(verbose_name="FFDI Max", blank=True) gfdi_min = models.PositiveIntegerField(verbose_name="GFDI Min", blank=True) gfdi_max = models.PositiveIntegerField(verbose_name="GFDI Max", blank=True) grassland_curing_min = models.PositiveIntegerField( verbose_name="Grassland Curing Min", blank=True) grassland_curing_max = models.PositiveIntegerField( verbose_name="Grassland Curing Max", blank=True) ros_min = models.PositiveIntegerField(verbose_name="ROS Min (m/h)", blank=True) ros_max = models.PositiveIntegerField(verbose_name="ROS Max (m/h)", blank=True) resources = models.TextField(verbose_name="Specialist Resources", blank=True) _required_fields = ('seqno', 'cellname', 'strategies', 'fuel_description', 'fuel_age', 'fuel_age_unknown', 'ignition_types', 'ffdi_min', 'ffdi_max', 'ros_min', 'ros_max', 'wind_min', 'wind_max', 'wind_dir') class Meta: verbose_name = "Lighting Sequence" verbose_name_plural = "Lighting Sequences" ordering = ['id'] def wind_speed(self): return field_range(self.wind_min, self.wind_max) wind_speed.short_description = mark_safe( '<abbr title="Wind Speed">Wind Speed</abbr> Range') def ffdi(self): return field_range(self.ffdi_min, self.ffdi_max) ffdi.short_description = mark_safe( '<abbr title="Forecast Fire Danger Index">FFDI</abbr> Range') def gfdi(self): return field_range(self.gfdi_min, self.gfdi_max) gfdi.short_description = mark_safe( '<abbr title="Grassland Fire Danger Index">FFDI</abbr> Range') gfdi.admin_order_field = "gfdi_max" def grassland_curing(self): return field_range(self.grassland_curing_min, self.grassland_curing_max) grassland_curing.short_description = mark_safe( '<abbr title="Grassland Curing Percent</abbr> Range') grassland_curing.admin_order_field = "grassland_curing_max" def ros(self): return field_range(self.ros_min, self.ros_max) ros.short_description = mark_safe( '<abbr title="Rate of Spread">ROS</abbr> Range') def clean_fuel_age(self): if self.fuel_age_unknown and self.fuel_age: raise ValidationError("You must either enter a fuel age or tick " "Fuel Age Unknown.") if not (self.fuel_age_unknown or self.fuel_age): raise ValidationError("You must enter a fuel age or tick Fuel Age " "Unknown.") def clean_ffdi_min(self): if self.ffdi_min is None: self.ffdi_min = 0 def clean_ffdi_max(self): if self.ffdi_max is None: self.ffdi_max = 0 def clean_gfdi_min(self): if self.gfdi_min is None: self.gfdi_min = 0 def clean_gfdi_max(self): if self.gfdi_max is None: self.gfdi_max = 0 def clean_grassland_curing_min(self): if self.grassland_curing_min is None: self.grassland_curing_min = 0 def clean_grassland_curing_max(self): if self.grassland_curing_max is None: self.grassland_curing_max = 0 def clean_ros_min(self): if self.ros_min is None: self.ros_min = 0 def clean_ros_max(self): if self.ros_max is None: self.ros_max = 0 def clean_wind_min(self): if self.wind_min is None: self.wind_min = 0 def clean_wind_max(self): if self.wind_max is None: self.wind_max = 0 def __str__(self): return "{0}. {1}".format(self.seqno, self.cellname)
class EdgingPlan(Audit): prescription = models.ForeignKey( Prescription, help_text="Prescription this edging plan belongs to.", on_delete=models.PROTECT) location = models.TextField( verbose_name="Edge Location", blank=True, null=True, help_text="Textual description of edge & its location") desirable_season = models.PositiveSmallIntegerField( verbose_name="Desirable Season", max_length=64, choices=Season.SEASON_CHOICES, blank=True, null=True) strategies = models.TextField( help_text="Textual description of strategies for this plan", blank=True, null=True) # NOTE: the fuel_type field will be deprecated in favour of a reference to # the VegetationType model in the prescription app. fuel_type = models.ForeignKey(FuelType, verbose_name="Fuel Type", blank=True, null=True, on_delete=models.PROTECT) ffdi_min = models.PositiveIntegerField(verbose_name="Min FFDI", blank=True, null=True) ffdi_max = models.PositiveIntegerField(verbose_name="Max FFDI", blank=True, null=True) gfdi_min = models.PositiveIntegerField(verbose_name="Min GFDI", blank=True, null=True) gfdi_max = models.PositiveIntegerField(verbose_name="Max GFDI", blank=True, null=True) sdi = models.TextField(verbose_name="SDI", blank=True, null=True) wind_min = models.PositiveIntegerField( verbose_name="Min Wind Speed (km/h)", blank=True, null=True) wind_max = models.PositiveIntegerField( verbose_name="Max Wind speed (km/h)", blank=True, null=True) wind_dir = models.TextField(verbose_name="Wind Direction", blank=True, null=True) ros_min = models.PositiveIntegerField(verbose_name="Min ROS (m/h)", blank=True, null=True) ros_max = models.PositiveIntegerField(verbose_name="Max ROS (m/h)", blank=True, null=True) grassland_curing_min = models.PositiveIntegerField( verbose_name="Grassland Curing % Min", blank=True, null=True) grassland_curing_max = models.PositiveIntegerField( verbose_name="Grassland Curing % Max", blank=True, null=True) def ffdi(self): return field_range(self.ffdi_min, self.ffdi_max) ffdi.short_description = mark_safe( '<abbr title="Forecast Fire Danger Index">FFDI</abbr> Range') ffdi.admin_order_field = 'ffdi_min' def gfdi(self): return field_range(self.gfdi_min, self.gfdi_max) gfdi.short_description = mark_safe( '<abbr title="Grassland Fire Danger Index">FFDI</abbr> Range') gfdi.admin_order_field = "gfdi_max" def wind(self): return "%d-%d" % (self.wind_min, self.wind_max) wind.short_description = "Wind Speed Range (km/h)" def grassland_curing(self): return field_range(self.grassland_curing_min, self.grassland_curing_max) grassland_curing.short_description = mark_safe( '<abbr title="Grassland Curing Percent</abbr> Range') grassland_curing.admin_order_field = "grassland_curing_max" def ros(self): return field_range(self.ros_min, self.ros_max) ros.short_description = mark_safe( '<abbr title="Rate of Spread">ROS</abbr> Range') def clean_sdi(self): if self.sdi == '': self.sdi = "N/A" def __str__(self): return self.location class Meta: verbose_name = "Edging Plan" verbose_name_plural = "Edging Plans" ordering = ['created']
class PrescribedBurn(Audit): BURN_ACTIVE = 1 BURN_INACTIVE = 2 BURN_MONITORED = 3 BURN_CHOICES = ( (BURN_ACTIVE, 'Yes'), (BURN_INACTIVE, 'No'), (BURN_MONITORED, 'Monitored'), ) IGNITION_STATUS_REQUIRED = 1 IGNITION_STATUS_COMPLETED = 2 IGNITION_STATUS_CHOICES = ( (IGNITION_STATUS_REQUIRED, 'Further ignitions required'), (IGNITION_STATUS_COMPLETED, 'Ignition now complete'), ) APPROVAL_DRAFT = 'DRAFT' APPROVAL_SUBMITTED = 'USER' APPROVAL_ENDORSED = 'SRM' APPROVAL_APPROVED = 'SDO' APPROVAL_CHOICES = ( (APPROVAL_DRAFT, 'Draft'), (APPROVAL_SUBMITTED, 'District Submitted'), (APPROVAL_ENDORSED, 'Region Endorsed'), (APPROVAL_APPROVED, 'State Approved'), ) FORM_268A = 1 FORM_268B = 2 FORM_NAME_CHOICES = ( (FORM_268A, 'Form 268a'), (FORM_268B, 'Form 268b'), ) ''' BUSHFIRE_DISTRICT_ALIASES can be used to override the District's original code from the model. Usage e.g.: BUSHFIRE_DISTRICT_ALIASES = { 'PHS' : 'PH', 'SWC' : 'SC', } ''' BUSHFIRE_DISTRICT_ALIASES = {} fmt = "%Y-%m-%d %H:%M" prescription = models.ForeignKey(Prescription, verbose_name="Burn ID", related_name='prescribed_burn', null=True, blank=True, on_delete=models.PROTECT) # prescription = ChainedForeignKey( # Prescription, chained_field="region", chained_model_field="region", # show_all=False, auto_choose=True, blank=True, null=True) # Required for Fire records fire_id = models.CharField(verbose_name="Fire Number", max_length=15, null=True, blank=True) fire_name = models.TextField(verbose_name="Name", null=True, blank=True) region = models.PositiveSmallIntegerField(choices=[ (r.id, r.name) for r in Region.objects.all() ], null=True, blank=True) district = ChainedForeignKey(District, chained_field="region", chained_model_field="region", show_all=False, auto_choose=True, blank=True, null=True, on_delete=models.PROTECT) fire_tenures = models.ManyToManyField(FireTenure, verbose_name="Tenures", blank=True) date = models.DateField(auto_now_add=False) form_name = models.PositiveSmallIntegerField( verbose_name="Form Name (268a / 268b)", choices=FORM_NAME_CHOICES, editable=True) status = models.PositiveSmallIntegerField(verbose_name="Active", choices=BURN_CHOICES, null=True, blank=True) ignition_status = models.PositiveSmallIntegerField( verbose_name="Ignition Status", choices=IGNITION_STATUS_CHOICES, null=True, blank=True) external_assist = models.ManyToManyField( ExternalAssist, verbose_name="Assistance received from", blank=True) planned_area = models.DecimalField( verbose_name="Today's treatment area (ha)", max_digits=12, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) area = models.DecimalField(verbose_name="Yesterday's treatment area (ha)", max_digits=12, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) planned_distance = models.DecimalField( verbose_name="Today's treatment distance (km)", max_digits=12, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) distance = models.DecimalField( verbose_name="Yesterday's treatment distance (km)", max_digits=12, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) tenures = models.TextField(verbose_name="Tenure") location = models.TextField(verbose_name="Location", null=True, blank=True) est_start = models.TimeField('Estimated Start Time', null=True, blank=True) conditions = models.TextField(verbose_name='SDO Special Conditions', null=True, blank=True) rolled = models.BooleanField(verbose_name="Fire Rolled from yesterday", editable=False, default=False) latitude = models.FloatField(default=0.0) longitude = models.FloatField(default=0.0) #aircraft_burn = models.BooleanField(verbose_name="Aircraft Burn", default=False) def clean(self): if not self.form_name: if self.prescription and self.area == None and self.distance == None: self.form_name = 1 else: self.form_name = 2 if self.prescription and not (self.region and self.district): self.region = self.prescription.region.id self.district = self.prescription.district def clean_fire_id(self): # set the Lat/Long to Zero, since Bushfire is not assigning these required fields self.latitude = 0.0 self.longitude = 0.0 # def clean_date(self): # today = date.today() # tomorrow = today + timedelta(days=1) # if not self.pk and (self.date < today or self.date > tomorrow): # raise ValidationError("You must enter burn plans for today or tommorow's date only.") def clean_planned_distance(self): if self.planned_area == None and self.planned_distance == None: raise ValidationError( "Must input at least one of Area or Distance") def clean_distance(self): if self.area == None and self.distance == None: raise ValidationError( "Must input at least one of Area or Distance") @property def is_acknowledged(self): if all(x in [i.acknow_type for i in self.acknowledgements.all()] for x in ['SDO_A', 'SDO_B']): return True else: return False @property def user_a_record(self): ack = self.acknowledgements.filter(acknow_type='USER_A') return ack[0].record if ack else None @property def srm_a_record(self): ack = self.acknowledgements.filter(acknow_type='SRM_A') return ack[0].record if ack else None @property def sdo_a_record(self): ack = self.acknowledgements.filter(acknow_type='SDO_A') return ack[0].record if ack else None @property def user_b_record(self): ack = self.acknowledgements.filter(acknow_type='USER_B') return ack[0].record if ack else None @property def srm_b_record(self): ack = self.acknowledgements.filter(acknow_type='SRM_B') return ack[0].record if ack else None @property def sdo_b_record(self): ack = self.acknowledgements.filter(acknow_type='SDO_B') return ack[0].record if ack else None @property def formA_isDraft(self): return not any( x in [i.acknow_type for i in self.acknowledgements.all()] for x in ['USER_A', 'SRM_A', 'SDO_A']) @property def formB_isDraft(self): return not any( x in [i.acknow_type for i in self.acknowledgements.all()] for x in ['USER_B', 'SRM_B', 'SDO_B']) @property def formA_user_acknowledged(self): return True if self.user_a_record else False @property def formA_srm_acknowledged(self): return True if self.srm_a_record else False @property def formA_sdo_acknowledged(self): return True if self.sdo_a_record else False @property def formB_user_acknowledged(self): return True if self.user_b_record else False @property def formB_srm_acknowledged(self): return True if self.srm_b_record else False @property def formB_sdo_acknowledged(self): return True if self.sdo_b_record else False @property def fire_type(self): return "Burn" if self.prescription else "Fire" @property def fire_idd(self): if self.prescription: return self.prescription.burn_id else: return self.fire_id @property def further_ignitions_req(self): if self.ignition_status == self.IGNITION_STATUS_REQUIRED: return True elif self.ignition_status == self.IGNITION_STATUS_COMPLETED: return False return None @property def active(self): # if self.status==self.BURN_ACTIVE: if self.status == self.BURN_ACTIVE or self.status == self.BURN_MONITORED: return True elif self.status == self.BURN_INACTIVE: return False return None @property def has_conditions(self): if self.conditions: return True return False @property def planned_area_str(self): _str = '' if self.planned_area: _str += str(self.planned_area) + " ha {} ".format( '-' if self.planned_distance else '') if self.planned_distance: _str += str(self.planned_distance) + " km" return _str @property def area_str(self): _str = '' if self.area >= 0: _str += str( self.area) + " ha {} ".format('-' if self.distance else '') if self.distance >= 0: _str += str(self.distance) + " km" return _str @property def tenures_str(self): if self.prescription: return self.tenures #', '.join([t.name for t in self.tenures.all()]) else: return ', '.join([i.name for i in self.fire_tenures.all()]) @property def had_external_assist(self): if self.external_assist.all().count() > 0: return True return False @property def external_assist_str(self): return ', '.join([i.name for i in self.external_assist.all()]) @property def name(self): if self.prescription: return self.prescription.name else: return self.fire_name @property def get_region(self): return self.prescription.region if self.prescription else self.region @property def get_district(self): return self.prescription.district if self.prescription else self.district @property def can_endorse(self): return (self.status == self.APPROVAL_SUBMITTED) @property def can_approve(self): return (self.status == self.APPROVAL_ENDORSED) @property def last_ignition(self): if self.prescription: area_achievements = self.prescription.areaachievement_set.all() if area_achievements: return max([i.ignition for i in area_achievements]) return None def short_str(self): return self.prescription.burn_id if self.prescription else self.fire_id def copy_ongoing_records(self, dt): """ Copy today's 'active' records to tomorrow 268b - (Automatically) copy all records from yesterday that were Active when 268a Region Endorsement occurs, except for Active and Area Burnt Yesterday dt = from date, copied to dt+1 """ tomorrow = dt + timedelta(days=1) # relative to dt #objects = [obj for obj in PrescribedBurn.objects.filter(date=dt, status=PrescribedBurn.BURN_ACTIVE)] objects = [self] now = timezone.now() admin = User.objects.get(username='******') count = 0 for obj in objects: if obj.fire_id and PrescribedBurn.objects.filter( fire_id=obj.fire_id, date=tomorrow, form_name=PrescribedBurn.FORM_268B): # don't copy if already exists - since record is unique on Prescription (not fire_id) logger.info( 'Ongoing Record Already Exists (Fire) - not copied (268b today to 268b tomorrow). Record {}, today {}, tomorrow {}' .format(obj.fire_idd, dt, tomorrow)) continue if obj.prescription and PrescribedBurn.objects.filter( prescription__burn_id=obj.prescription.burn_id, date=tomorrow, form_name=PrescribedBurn.FORM_268B, location=obj.location): # don't copy if already exists - since record is unique on Prescription (not fire_id) logger.info( 'Ongoing Record Already Exists (Burn) - not copied (268b today to 268b tomorrow). Record {}, today {}, tomorrow {}' .format(obj.fire_idd, dt, tomorrow)) continue try: obj.pk = None obj.date = tomorrow obj.area = None obj.status = None obj.approval_268a_status = PrescribedBurn.APPROVAL_DRAFT obj.approval_268a_status_modified = now obj.approval_268b_status = PrescribedBurn.APPROVAL_DRAFT obj.approval_268b_status_modified = now obj.acknowledgements.all().delete() obj.rolled = True obj.save() count += 1 logger.info( 'Ongoing Record copied (268b today to 268b tomorrow). Record {}, today {}, tomorrow {}' .format(obj.fire_idd, dt, tomorrow)) except: # records already exist - pk (pres, date) will not allow overwrite, so ignore the exception logger.warn( 'Ongoing Record not copied. Record {} already exists on day {}' .format(obj.fire_idd, tomorrow)) def copy_planned_approved_records_adhoc(self, dt): """ Copy today's 'planned' records (268a), that have been SDO approved. to tomorrow ongoing (268b) set Active and Area Burnt fields to None dt = from date, copied to dt+1 """ tomorrow = dt + timedelta(days=1) # relative to dt if not self.formA_sdo_acknowledged: logger.info( 'Only SDO Acknowledged record can be copied from dt {} to tomorrow {}' .format(dt, tomorrow)) return #objects = PrescribedBurn.objects.filter(date=dt, acknowledgements__acknow_type__in=['SDO_A'], form_name=PrescribedBurn.FORM_268A) objects = [self] now = timezone.now() count = 0 for obj in objects: if obj.fire_id and PrescribedBurn.objects.filter( fire_id=obj.fire_id, date=tomorrow, form_name=PrescribedBurn.FORM_268B): # don't copy if already exists - since record is unique on Prescription (not fire_id) logger.info( 'Planned Approved Record Already Exists (Fire) - not copied (268a today to 268b tomorrow). Record {}, today {}, tomorrow {}' .format(obj.fire_idd, dt, tomorrow)) continue if obj.prescription and PrescribedBurn.objects.filter( prescription__burn_id=obj.prescription.burn_id, date=tomorrow, form_name=PrescribedBurn.FORM_268B, location=obj.location): # don't copy if already exists - since record is unique on Prescription (not fire_id) logger.info( 'Planned Approved Record Already Exists (Burn) - not copied (268a today to 268b tomorrow). Record {}, today {}, tomorrow {}' .format(obj.fire_idd, dt, tomorrow)) continue try: obj.pk = None obj.date = tomorrow obj.area = None obj.distance = None obj.status = None obj.approval_268a_status = PrescribedBurn.APPROVAL_DRAFT obj.approval_268a_status_modified = now obj.approval_268b_status = PrescribedBurn.APPROVAL_DRAFT obj.approval_268b_status_modified = now #obj.acknowledgements.all().delete() obj.form_name = PrescribedBurn.FORM_268B obj.rolled = True obj.save() count += 1 logger.info( 'Planned Approved Record copied (268a today to 268b tomorrow). Record {}, today {}, tomorrow {}' .format(obj.fire_idd, dt, tomorrow)) except: # records already exist - pk (pres, date) will not allow overwrite, so ignore the exception logger.warn( 'Planned Approved Record not copied. Record {} already exists on day {}' .format(obj.fire_idd, tomorrow)) def __str__(self): return self.prescription.burn_id + ' (Burn)' if self.prescription else self.fire_id + ' (Fire)' class Meta: unique_together = ('prescription', 'date', 'form_name', 'location') verbose_name = 'Prescribed Burn or Bushfire' verbose_name_plural = 'Prescribed Burns and Bushfires' permissions = ( ("can_endorse", "Can endorse burns"), ("can_approve", "Can approve burns"), )
class BurningPrescription(Audit): prescription = models.ForeignKey( Prescription, help_text="Prescription this fuel schedule belongs to.", on_delete=models.PROTECT) # NOTE: the fuel_type field will be deprecated in favour of a reference to # the VegetationType model in the prescription app. fuel_type = models.ForeignKey(FuelType, verbose_name="Fuel Type", blank=True, null=True, on_delete=models.PROTECT) scorch = models.PositiveIntegerField( help_text="Maximum Scorch Height (m)", verbose_name="Scorch Height", validators=[MinValueValidator(1), MaxValueValidator(30)], blank=True, null=True) min_area = models.PositiveIntegerField( verbose_name="Min Area to be Burnt (%)", validators=[MinValueValidator(1), MaxValueValidator(100)], blank=True, null=True) max_area = models.PositiveIntegerField( verbose_name="Max Area to be Burnt (%)", validators=[MinValueValidator(1), MaxValueValidator(100)], blank=True, null=True) ros_min = models.PositiveIntegerField( verbose_name="Min ROS (m/h)", validators=[MinValueValidator(0), MaxValueValidator(10000)], blank=True, null=True) ros_max = models.PositiveIntegerField( verbose_name="Max ROS (m/h)", validators=[MinValueValidator(0), MaxValueValidator(10000)], blank=True, null=True) ffdi_min = models.PositiveIntegerField( verbose_name="Min FFDI", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) ffdi_max = models.PositiveIntegerField( verbose_name="Max FFDI", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) gfdi_min = models.PositiveIntegerField( verbose_name="Min GFDI", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) gfdi_max = models.PositiveIntegerField( verbose_name="Max GFDI", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) temp_min = models.PositiveIntegerField( verbose_name="Min Temp (degrees C)", validators=[MinValueValidator(0), MaxValueValidator(60)], blank=True, null=True) temp_max = models.PositiveIntegerField( verbose_name="Max Temp (degress C)", validators=[MinValueValidator(0), MaxValueValidator(60)], blank=True, null=True) rh_min = models.PositiveIntegerField( verbose_name="Min Relative Humidity (%)", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) rh_max = models.PositiveIntegerField( verbose_name="Max Relative Humidity (%)", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) sdi = models.TextField(verbose_name="SDI", blank=True, null=True) smc_min = models.PositiveIntegerField( verbose_name="Min Surface Moisture Content (%)", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) smc_max = models.PositiveIntegerField( verbose_name="Max Surface Moisture Content (%)", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) pmc_min = models.PositiveIntegerField( verbose_name="Min Profile Moisture Content (%)", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) pmc_max = models.PositiveIntegerField( verbose_name="Max Profile Moisture Content (%)", validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, null=True) wind_min = models.PositiveIntegerField( verbose_name="Min Wind Speed (km/h)", blank=True, null=True, validators=[MinValueValidator(0), MaxValueValidator(200)]) wind_max = models.PositiveIntegerField( verbose_name="Max Wind Speed (km/h)", validators=[MinValueValidator(0), MaxValueValidator(200)], blank=True, null=True) wind_dir = models.TextField(verbose_name="Wind Direction", blank=True, null=True) grassland_curing_min = models.PositiveIntegerField( verbose_name="Grassland Curing % Min", validators=[MinValueValidator(0), MaxValueValidator(200)], blank=True, null=True) grassland_curing_max = models.PositiveIntegerField( verbose_name="Grassland Curing % Max", validators=[MinValueValidator(0), MaxValueValidator(200)], blank=True, null=True) def area(self): return field_range(self.min_area, self.max_area, True) area.short_description = "Area to be Burnt (%)" area.admin_order_field = "area_max" def ros(self): return field_range(self.ros_min, self.ros_max) ros.short_description = mark_safe( '<abbr title="Rate of Spread">ROS</abbr> Range') ros.admin_order_field = "ros_max" def ffdi(self): return field_range(self.ffdi_min, self.ffdi_max) ffdi.short_description = mark_safe( '<abbr title="Forecast Fire Danger Index">FFDI</abbr> Range') ffdi.admin_order_field = "ffdi_max" def gfdi(self): return field_range(self.gfdi_min, self.gfdi_max) gfdi.short_description = mark_safe( '<abbr title="Global Fire Danger Index">GFDI</abbr> Range') gfdi.admin_order_field = "gfdi_max" def temp(self): return field_range(self.temp_min, self.temp_max) temp.short_description = "Temperature Range" temp.admin_order_field = "temp_max" def rh(self): return field_range(self.rh_min, self.rh_max, True) rh.short_description = mark_safe( '<abbr title="Relative Humidity">RH</abbr> Range (%)') temp.admin_order_field = "temp_max" sdi.short_description = mark_safe( '<abbr title="Soil Dryness Index">SDI</abbr> Range') def smc(self): return field_range(self.smc_min, self.smc_max) smc.short_description = mark_safe( '<abbr title="Surface Moisture Content">SMC</abbr> Range') smc.admin_order_field = "smc_max" def pmc(self): return field_range(self.pmc_min, self.pmc_max) pmc.short_description = mark_safe( '<abbr title="Profile Moisture Content">PMC</abbr> Range') pmc.admin_order_field = "pmc_max" def wind(self): return field_range(self.wind_min, self.wind_max) wind.short_description = "Wind Speed Range (km/h)" wind.admin_order_field = "wind_max" def grassland_curing(self): return field_range(self.grassland_curing_min, self.grassland_curing_max) grassland_curing.short_description = mark_safe( '<abbr title="Grassland Curing Percent">GLC</abbr> Range') grassland_curing.admin_order_field = "grassland_curing_max" def clean_sdi(self): if self.sdi == '': self.sdi = "N/A" def __str__(self): return self.fuel_type.name class Meta: verbose_name = "Burning Prescription" verbose_name_plural = "Burning Prescriptions"
class GrandParentDuck(Audit): """A parent duck.""" name = models.CharField(max_length=20) duck = models.ForeignKey(ParentDuck) objects = Manager()
class BurnProgramLink(models.Model): prescription = models.ForeignKey(Prescription, unique=True) wkb_geometry = models.MultiPolygonField(srid=4326) area_ha = models.FloatField() longitude = models.FloatField() latitude = models.FloatField() perim_km = models.FloatField() trtd_area = models.FloatField() @classmethod def populate(cls): # Links prescriptions to burn program records imported using ogr2ogr import subprocess subprocess.check_call([ 'ogr2ogr', '-overwrite', '-f', 'PostgreSQL', "PG:dbname='{NAME}' host='{HOST}' port='{PORT}' user='******' password={PASSWORD}" .format(**settings.DATABASES["default"]), settings.ANNUAL_INDIC_PROGRAM_PATH, settings.SHP_LAYER, '-nln', 'review_annualindicativeburnprogram', '-nlt', 'PROMOTE_TO_MULTI', '-t_srs', 'EPSG:4326', '-lco', 'GEOMETRY_NAME=wkb_geometry' ]) for p in AnnualIndicativeBurnProgram.objects.all(): try: for prescription in Prescription.objects.filter( burn_id=p.burnid, financial_year=p.fin_yr.replace("/", "/20")): if cls.objects.filter(prescription=prescription).exists(): obj = cls.objects.get(prescription=prescription) else: obj = cls(prescription=prescription) obj.wkb_geometry = p.wkb_geometry obj.area_ha = p.area_ha obj.longitude = p.longitude obj.latitude = p.latitude obj.perim_km = p.perim_km obj.trtd_area = p.trtd_area if p.trtd_area and isinstance( p.trtd_area, float) else 0.0 obj.save() except: logger.error( 'ERROR: Assigning AnnulaIndicativeBurnProgram \n{}'.format( sys.exc_info())) from django.db import connection cursor = connection.cursor() cursor.execute(''' create or replace view review_v_dailyburns as select p.burn_id, to_char(pb.date, 'FMDay, DD Mon YYYY') as burn_target_date, pb.date as burn_target_date_raw, case when string_agg(pb.form_name::text, ', ') = '1, 2' or string_agg(pb.form_name::text, ', ') = '2, 1' then 'Active - Planned Ignitions Today' when string_agg(pb.form_name::text, ', ') = '2' then 'Active - No Planned Ignitions Today' when string_agg(pb.form_name::text, ', ') = '1' then 'Planned - No Prior Ignitions' else 'Error' end as burn_stat, case when pb.location like '%|%' then case when p.forest_blocks not like '' then split_part(pb.location, '|', 1) || ', ' || split_part(pb.location, '|', 2) || 'km ' || split_part(pb.location, '|', 3) || ' of '|| split_part(pb.location, '|', 4) || ' (' || p.forest_blocks || ')' else split_part(pb.location, '|', 1) || ', ' || split_part(pb.location, '|', 2) || 'km ' || split_part(pb.location, '|', 3) || ' of '|| split_part(pb.location, '|', 4) end else case when p.forest_blocks not like '' then pb.location || ' (' || p.forest_blocks || ')' else pb.location end end as location, p.forest_blocks, link.area_ha AS indicative_area, coalesce( (select rpb.est_start from review_prescribedburn rpb where rpb.date = pb.date and rpb.prescription_id::text = pb.prescription_id::text and rpb.form_name = 1 and rpb.location = pb.location)::text, '') AS burn_est_start, -- use time from 268a coalesce( (select rpb.longitude from review_prescribedburn rpb where rpb.date = pb.date and rpb.prescription_id::text = pb.prescription_id::text and rpb.form_name = 1 and rpb.location = pb.location), pb.longitude) AS burn_target_long, -- use longitude from 268a, else 268b coalesce( (select rpb.latitude from review_prescribedburn rpb where rpb.date = pb.date and rpb.prescription_id::text = pb.prescription_id::text and rpb.form_name = 1 and rpb.location = pb.location), pb.latitude) AS burn_target_lat, -- use latitude from 268a, else 268b coalesce( cast((select rpb.planned_area from review_prescribedburn rpb where rpb.date = pb.date and rpb.prescription_id::text = pb.prescription_id::text and rpb.form_name = 1 and rpb.location = pb.location) as text), '') AS burn_planned_area_today, -- use planned_area from 268a coalesce( cast((select rpb.planned_distance from review_prescribedburn rpb where rpb.date = pb.date and rpb.prescription_id::text = pb.prescription_id::text and rpb.form_name = 1 and rpb.location = pb.location) as text), '') AS burn_planned_distance_today, -- use planned_distance from 268a link.wkb_geometry, coalesce((SELECT array_to_string(array_agg(pp.name),' , ') FROM prescription_prescription_purposes ppps JOIN prescription_purpose pp ON ppps.purpose_id = pp.id WHERE ppps.prescription_id = p.id) ,'') AS burn_purpose from (((prescription_prescription p LEFT JOIN review_prescribedburn pb ON ((p.id = pb.prescription_id))) LEFT JOIN review_acknowledgement ack ON ((pb.id = ack.burn_id))) LEFT JOIN review_burnprogramlink link ON ((p.id = link.prescription_id))) WHERE ( (((ack.acknow_type)::text = 'SDO_A'::text) AND (pb.form_name = 1)) OR -- approved 268a ((((ack.acknow_type)::text = 'SDO_B'::text) AND (pb.form_name = 2)) AND (pb.status = 1))) -- approved active 268b group by p.id,p.burn_id, pb.location, p.forest_blocks, burn_target_date, indicative_area, burn_target_long, burn_target_lat, burn_est_start, link.wkb_geometry, burn_planned_area_today, burn_planned_distance_today, burn_target_date_raw ORDER BY p.burn_id, burn_target_date_raw; create or replace view review_v_todaysburns as select * from review_v_dailyburns where burn_target_date_raw = current_date; create or replace view review_v_yesterdaysburns as select * from review_v_dailyburns where burn_target_date_raw = current_date - interval '1 day'; CREATE OR REPLACE FUNCTION review_f_lastdaysburns() RETURNS setof review_v_dailyburns AS $$ DECLARE last_date review_v_dailyburns.burn_target_date_raw%TYPE; BEGIN SELECT max(pb.date) INTO last_date FROM review_prescribedburn pb LEFT JOIN review_acknowledgement ack ON pb.id = ack.burn_id WHERE ( (((ack.acknow_type)::text = 'SDO_A'::text) AND (pb.form_name = 1)) OR ((((ack.acknow_type)::text = 'SDO_B'::text) AND (pb.form_name = 2)) AND (pb.status = 1)) ) AND (pb.date < current_date); IF last_date IS NULL THEN RETURN QUERY SELECT * FROM review_v_dailyburns WHERE false; ELSE RETURN QUERY SELECT * FROM review_v_dailyburns WHERE burn_target_date_raw = last_date; END IF; END; $$ LANGUAGE plpgsql; create or replace view review_v_lastdaysburns as select * from review_f_lastdaysburns(); ''')
class AircraftBurn(Audit): APPROVAL_DRAFT = 'DRAFT' APPROVAL_SUBMITTED = 'USER' APPROVAL_ENDORSED = 'SRM' APPROVAL_APPROVED = 'SDO' APPROVAL_CHOICES = ( (APPROVAL_DRAFT, 'Draft'), (APPROVAL_SUBMITTED, 'District Submitted'), (APPROVAL_ENDORSED, 'Region Endorsed'), (APPROVAL_APPROVED, 'State Approved'), ) fmt = "%Y-%m-%d %H:%M" prescription = models.ForeignKey(Prescription, verbose_name="Burn ID", related_name='aircraft_burns', null=True, blank=True, on_delete=models.PROTECT) #prescribed_burn = models.ForeignKey(PrescribedBurn, verbose_name="Daily Burn ID", related_name='aircraft_burn', null=True, blank=True) date = models.DateField(auto_now_add=False) area = models.DecimalField(verbose_name="Area (ha)", max_digits=12, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) est_start = models.TimeField('Estimated Start Time', null=True, blank=True) bombing_duration = models.DecimalField( verbose_name="Bombing Duration (hrs)", max_digits=5, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) min_smc = models.DecimalField(verbose_name="Min SMC", max_digits=5, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) max_fdi = models.DecimalField(verbose_name="Max FDI", max_digits=5, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) sdi_per_day = models.DecimalField(verbose_name="SDI Each Day", max_digits=5, decimal_places=1, validators=[MinValueValidator(0.0)], null=True, blank=True) flight_seq = models.TextField(verbose_name="Flight Sequence", null=True, blank=True) aircraft_rego = models.TextField(verbose_name="Aircraft Rego", null=True, blank=True) arrival_time = models.TimeField(verbose_name="Arrival Time Over Burn", null=True, blank=True) program = models.TextField(verbose_name="Program", null=True, blank=True) aircrew = models.TextField(verbose_name="Aircrew", null=True, blank=True) rolled = models.BooleanField(verbose_name="Fire Rolled from yesterday", editable=False, default=False) #location= models.TextField(verbose_name="Location", null=True, blank=True) @property def regional_approval(self): return True @property def state_duty_approval(self): return True @property def state_aviation_approval(self): return True def __str__(self): return self.prescription.burn_id class Meta: unique_together = ('prescription', 'date') verbose_name = 'Aircraft Burn' verbose_name_plural = 'Aircraft Burns' permissions = ( ("can_endorse", "Can endorse burns"), ("can_approve", "Can approve burns"), )