class Medium(CreateModifyOn): project = models.ForeignKey( Project, help_text='Project that this medium belongs to', on_delete=models.PROTECT) received_date = models.DateField( help_text='Date that the medium was received') photographer = models.ForeignKey( PhysicalPerson, help_text='Person who took the photo/video', on_delete=models.PROTECT) license = models.ForeignKey(License, help_text='License', on_delete=models.PROTECT, null=True, blank=True) copyright = models.CharField( max_length=1024, help_text= 'Owner of copyright if it is not the photographer (e.g. institution)', null=True, blank=True) file = models.FileField(storage=S3Boto3Storage(), upload_to=medium_file_rename, validators=[*management_file_validator()]) file_md5 = models.CharField(max_length=32, null=True, blank=True) blog_posts = models.ManyToManyField( BlogPost, help_text='Which blog posts this image belongs to', blank=True) descriptive_text = models.TextField( help_text= 'Description of this media, if provided. Where was it taken, context, etc.', null=True, blank=True) class Meta: verbose_name_plural = 'Media' def __str__(self): return f'{self.project}-{self.photographer}' def save(self, *args, **kwargs): self.file_md5 = calculate_md5_from_file_field(self.file) super().save(*args, **kwargs) def delete(self, *args, **kwargs): with transaction.atomic(): MediumDeleted.objects.create(**{'original_id': self.id}) delete_result = super().delete(*args, **kwargs) return delete_result
class ProjectAttachment(AbstractAttachment): file = models.FileField(storage=S3Boto3Storage(), upload_to=project_attachment_rename, validators=[*management_file_validator()]) project = models.ForeignKey(Project, help_text='Project that this attachment belongs to', on_delete=models.PROTECT) category = models.ForeignKey(ProjectAttachmentCategory, help_text='Category of the attachment', on_delete=models.PROTECT) def set_parent(self, parent): self.project = parent @staticmethod def category_queryset(): return ProjectAttachmentCategory.objects.all()
class CallAttachment(AbstractAttachment): file = models.FileField(storage=S3Boto3Storage(), upload_to=call_attachment_rename, validators=[*management_file_validator()]) call = models.ForeignKey(Call, help_text='Call that this attachment belongs to', on_delete=models.PROTECT) category = models.ForeignKey(CallAttachmentCategory, help_text='Category of the attachment', on_delete=models.PROTECT) def __str__(self): return f'Call:{self.call.little_name()} Category:{self.category} File: {self.file.name}' def set_parent(self, parent): self.call = parent @staticmethod def category_queryset(): return CallAttachmentCategory.objects.all()
class GrantAgreementAttachment(AbstractAttachment): file = models.FileField(storage=S3Boto3Storage(), upload_to=grant_agreement_attachment_rename, validators=[*management_file_validator()]) grant_agreement = models.ForeignKey(GrantAgreement, help_text='GrantAgreement that this attachment belongs to', on_delete=models.PROTECT) category = models.ForeignKey(GrantAgreementAttachmentCategory, help_text='Category of the attachment', on_delete=models.PROTECT) class Meta: verbose_name_plural = 'Grant Agreement Attachments' def set_parent(self, parent): self.grant_agreement = parent @staticmethod def category_queryset(): return GrantAgreementAttachmentCategory.objects.all()
class GrantAgreement(CreateModifyOn): project = models.OneToOneField( Project, help_text='Project this Grant Agreement belongs to', on_delete=models.PROTECT) signed_date = models.DateField( help_text='Date the grant agreement was signed', null=True, blank=True) signed_by = models.ManyToManyField( PhysicalPerson, help_text='People who signed the grant agreement', blank=True) file = models.FileField(storage=S3Boto3Storage(), upload_to=grant_agreement_file_rename, validators=[*management_file_validator()]) def __str__(self): return f'{self.project}' def signed_by_string(self): return ', '.join([ f'{person.first_name} {person.surname}' for person in self.signed_by.all().order_by('first_name') ]) @staticmethod def comment_object(): from comments.models import GrantAgreementComment return GrantAgreementComment @staticmethod def attachment_object(): from comments.models import GrantAgreementAttachment return GrantAgreementAttachment def attachments(self): return self.grantagreementattachment_set.all().order_by('created_on') def comments(self): return self.grantagreementcomment_set.all().order_by('created_on')
class ScientificReport(AbstractProjectReport): file = models.FileField(storage=S3Boto3Storage(), upload_to=scientific_report_file_rename, validators=[*management_file_validator()], blank=True, null=True)
class Invoice(AbstractProjectDueReceivedDate): sent_for_payment_date = models.DateField( help_text='Date the invoice was sent for payment', null=True, blank=True) file = models.FileField(storage=S3Boto3Storage(), upload_to=invoice_file_rename, null=True, validators=[*management_file_validator()], blank=True) paid_date = models.DateField(help_text='Date the invoice was paid', null=True, blank=True) amount = models.DecimalField(max_digits=20, decimal_places=2, help_text='Total of the invoice (CHF)', null=True, blank=True) installment = models.ForeignKey( Installment, help_text='Installment to which the invoice is assigned', null=True, blank=True, on_delete=models.PROTECT) allow_overbudget = models.BooleanField( default=False, help_text='This invoice takes a payment overbudget') overbudget_allowed_by = models.ForeignKey( User, null=True, blank=True, help_text='User that allowed the overbudget', on_delete=models.PROTECT) def __str__(self): return f'Id: {self.id} Amount: {self.amount}' @staticmethod def comment_object(): from comments.models import InvoiceComment return InvoiceComment @staticmethod def attachment_object(): return None def installment_number(self): # This is not very efficient, but given the number of invoices and installments it's nice to not have to # save this in the database if self.installment is None: return None return self.installment.number() def due_date_passed(self): return self.due_date and self.due_date < datetime.today().date( ) and self.paid_date is None def comments(self): return self.invoicecomment_set.all().order_by('created_on') def attachments(self): return None
class Medium(CreateModifyOn): project = models.ForeignKey( Project, help_text='Project that this medium belongs to', on_delete=models.PROTECT) received_date = models.DateField( help_text='Date that the medium was received') photographer = models.ForeignKey( PhysicalPerson, help_text='Person who took the photo/video', on_delete=models.PROTECT) license = models.ForeignKey(License, help_text='License', on_delete=models.PROTECT, null=True, blank=True) copyright = models.CharField( max_length=1024, help_text= 'Owner of copyright if it is not the photographer (e.g. institution)', null=True, blank=True) file = models.FileField(storage=S3Boto3Storage(), upload_to=medium_file_rename, validators=[*management_file_validator()]) file_md5 = models.CharField(max_length=32, null=True, blank=True) file_web = models.FileField(storage=S3Boto3Storage(), upload_to=medium_file_rename, null=True) blog_posts = models.ManyToManyField( BlogPost, help_text='Which blog posts this image belongs to', blank=True) descriptive_text = models.TextField( help_text= 'Description of this media, if provided. Where was it taken, context, etc.', null=True, blank=True) key_image = models.BooleanField( default=False, help_text="Select as a key image to be displayed on website") primary_image = models.BooleanField( default=False, help_text="Select as a primary image on website") class Meta: verbose_name_plural = 'Media' def __str__(self): return f'{self.project}-{self.photographer}' def save(self, *args, **kwargs): self.file_md5 = calculate_md5_from_file_field(self.file) # Be sure only one primary_image per project if self.primary_image: with transaction.atomic(): Medium.objects.filter( Q(primary_image=True) & Q(project=self.project)).update(primary_image=False) if self.file.name.lower().endswith(tuple(['jpg', 'jpeg', 'png'])): try: im = Image.open(self.file) if im._getexif(): orientation = 0 for orientation in ExifTags.TAGS.keys(): if ExifTags.TAGS[orientation] == 'Orientation': break exif = dict(im._getexif().items()) if orientation in exif.keys(): if exif[orientation] == 3: im = im.rotate(180, expand=True) elif exif[orientation] == 6: im = im.rotate(270, expand=True) elif exif[orientation] == 8: im = im.rotate(90, expand=True) out = BytesIO() if self.file.name.lower().endswith('png'): im = im.convert('RGB') im.save(out, format='JPEG', quality=75) out.seek(0) self.file_web = InMemoryUploadedFile( out, 'ImageField', "%s_web.jpeg" % self.file.name.split('.')[0], 'image/jpeg', sys.getsizeof(out), None) except (UnidentifiedImageError) as e: logger.warning( f"Error: {e}; Can not save smaller image for web!") super().save(*args, **kwargs) def delete(self, *args, **kwargs): with transaction.atomic(): MediumDeleted.objects.create(**{'original_id': self.id}) delete_result = super().delete(*args, **kwargs) return delete_result