Skip to content
Snippets Groups Projects
models.py 15.2 KiB
Newer Older
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
import os
Jonathan Seguin's avatar
Jonathan Seguin committed
import binascii
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed

Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
from django.conf import settings
from django.contrib.auth import get_user_model
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
from django.contrib.postgres.fields import HStoreField
from django.db import models
from django.db.models import Q
from django.template import defaultfilters
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
from django.utils.translation import get_language
from django.utils.translation import ugettext_lazy as _
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
from private_storage.fields import PrivateFileField
from portal.storage import HashStorage
class ProfileManager(models.Manager):
    def exclude_profile(self, profile):
        return super().get_queryset().exclude(id=profile.id)

class Profile(models.Model):
    """example Documentation:
    https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone
    """
    class Meta:
        ordering = ['user__last_name', 'user__first_name']
        verbose_name = _("user")

    accountname = models.CharField(max_length=150, null=True, blank=True)
    unix_username = models.CharField(max_length=150, null=True, blank=True, verbose_name=_("Unix username"))
    user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='profile')
    labs = models.ManyToManyField('Lab', related_name='members')

Jonathan Seguin's avatar
Jonathan Seguin committed
    api_token = models.CharField(max_length=64, null=True, unique=True, blank=False, default=None)

Jonathan Seguin's avatar
Jonathan Seguin committed
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
Jonathan Seguin's avatar
Jonathan Seguin committed
            self.api_token = self.generate_token()
            self.save()

    def __str__(self):
        return '{0}, {1} <{2}>'.format(self.user.last_name, self.user.first_name, self.user.email)

    @property
    def fullname(self):
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
        return self.user.get_full_name()
    @property
    def is_pi(self):
        return Lab.objects.filter(pi=self).exists()

    @property
    def is_data_manager(self):
        return Lab.objects.filter(data_managers=self).exists()

    @property
    def is_pi_or_data_manager(self):
        return Lab.objects.filter(Q(pi=self) | Q(data_managers=self)).exists()

Jonathan Seguin's avatar
Jonathan Seguin committed
    @staticmethod
    def generate_token():
        return binascii.hexlify(os.urandom(32)).decode()


class Institution(models.Model):
    class Meta:
        unique_together = (('abbr', 'name'))
        ordering = ['name']

    abbr = models.CharField(max_length=64, null=True, blank=True, unique=True, verbose_name=_('Abbreviation'))
    name = models.CharField(max_length=256, unique=True, verbose_name=_('Name'))

    def __str__(self):
        return self.name

    def get_identifier(self):
        if self.abbr:
            return self.abbr
        else:
            return self.name


class LabManager(models.Manager):
    def limit_to_profile(self, profile):
        return super().get_queryset().filter(members=profile)


class Lab(models.Model):
    class Meta:
        ordering = ['name']

    name = models.CharField(max_length=64, verbose_name=_('Name'))
    ldap = models.CharField(max_length=32, null=True, blank=True, verbose_name='LDAP')

Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    pi = models.OneToOneField(
        Profile,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        limit_choices_to={'user__groups__name': settings.AUTH_LDAP_PI_GROUP},
        related_name='pi_of')

    data_managers = models.ManyToManyField(Profile, blank=True, related_name='data_manager_of', verbose_name=_('Data managers'))
    institution = models.ForeignKey(Institution, on_delete=models.PROTECT, related_name='labs')

    def __str__(self):
        return self.name


class DataFileManager(models.Manager):
    def limit_to_profile(self, profile):
        return super().get_queryset().filter(uploaded_by=profile)

    def shared_with_profile(self, profile):
        return super().get_queryset().filter(
            Q(datasets__share_profiles=profile)
            | Q(datasets__share_labs__members=profile)
            | Q(datasets__share_groups__profiles=profile)

    def accessible_to_profile(self, profile):
        return super().get_queryset().filter(
            Q(uploaded_by=profile)
            | Q(datasets__share_profiles=profile)
            | Q(datasets__share_labs__members=profile)
            | Q(datasets__share_groups__profiles=profile)
            | Q(lab__data_managers=profile)
    def editable_by_profile(self, profile):
        return super().get_queryset().filter(
            Q(uploaded_by=profile)
            | Q(lab__pi=profile)
            | Q(lab__data_managers=profile)
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
# File Object ########
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
class DataFile(models.Model):
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    DataFile is the basic file object to which a number of annotations can be attached.
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    File always belongs to 1 (and only 1) lab.
    Access is granted to lab PI, Lab data managers and original Uploader.
    To grant further access, files must be bundled in 'DataSets' for which independent access can be granted
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    File annotations are handled trough the use of PostgreSQL's hstore datatype
    https://docs.djangoproject.com/en/2.2/ref/contrib/postgres/fields/#hstorefield
    https://docs.djangoproject.com/en/2.2/ref/contrib/postgres/operations/#create-postgresql-extensions
    Also of interest:
    https://www.citusdata.com/blog/2016/07/14/choosing-nosql-hstore-json-jsonb/
Jonathan Seguin's avatar
Jonathan Seguin committed
    class Meta:
        verbose_name = _("file")

    file = PrivateFileField(max_length=256, null=True, storage=HashStorage())
    annotations = HStoreField(verbose_name=_('File Annotations'), null=True, blank=True)
    upload_timestamp = models.DateTimeField(verbose_name=_('Upload Timestamp'), auto_now_add=True)
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed

    filename = models.CharField(max_length=256, verbose_name=_('Filename'))
    iric_data_id = models.CharField(max_length=10, verbose_name=_('ID'), null=True)

Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    uploaded_by = models.ForeignKey(Profile, on_delete=models.PROTECT, verbose_name=_('Uploaded by'), related_name='uploaded_files')
    lab = models.ForeignKey(Lab, null=True, blank=True, on_delete=models.PROTECT, related_name='files', verbose_name=_('Belongs to this lab'), default=None)
    objects = DataFileManager()

    def __str__(self):
        return f"{self.filename} ({self.iric_data_id})"
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    @property
    def filext(self):
        ext = os.path.splitext(self.filename)[-1][1:].lower()
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
        return ext

    @property
    def hash(self):
        return self.file.name[4:]

    def save(self, *args, **kwargs):
        if self.file and not self.filename:
            self.filename = self.file.name

        if self.pk is None or self.iric_data_id is None:
            super(DataFile, self).save(*args, **kwargs)
Jonathan Seguin's avatar
Jonathan Seguin committed
            self.iric_data_id = "DF{}".format(hashlib.md5(str(self.id).encode()).hexdigest()[:8]).upper()

        super(DataFile, self).save(*args, **kwargs)
Jonathan Seguin's avatar
Jonathan Seguin committed
        return self.filename
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    @property
    def placeholder_icon(self):
        default = 'fas fa-file-alt'
        assoc = {
            'pdf': 'fas fa-file-pdf',
            'doc': 'fas fa-file-word',
            'xls': 'fas fa-file-excel',
            'ppt': 'fas fa-file-powerpoint',
            'png': 'fas fa-file-image',
            'jpg': 'fas fa-file-image',
            'jpe': 'fas fa-file-image'
        }
        try:
            return assoc[self.filext[:3]]
        except KeyError:
            return default

    def delete(self, using=None, keep_parents=False):
        if DataFile.objects.filter(file=self.file.name).count() == 1:
    @property
    def share_profiles(self):
        """Proxy to get list of Profiles with which file is shared"""
        return Profile.objects.filter(shared_datasets__files=self.id).all()

    @property
    def share_labs(self):
        """Proxy to get list of Labs with which file is shared"""
        return Lab.objects.filter(shared_datasets__files=self.id).all()

    @property
    def share_groups(self):
        """Proxy to get list of ShareGroups with which file is shared"""
        return ShareGroup.objects.filter(shared_datasets__files=self.id).all()

class ShareGroupManager(models.Manager):
    def limit_to_profile(self, profile):
        return super().get_queryset().filter(profiles=profile)
class ShareGroup(models.Model):
    class Meta:
        ordering = ['name']

    name = models.CharField(max_length=64, verbose_name=_('Name'))
    creation_timestamp = models.DateTimeField(verbose_name=_('Creation Timestamp'), auto_now_add=True)
    update_timestamp = models.DateTimeField(verbose_name=_('Last Update Timestamp'), auto_now=True)
    last_update_by = models.ForeignKey(Profile, on_delete=models.PROTECT, verbose_name=_('Last Update By'), related_name='my_sharegroups')

    profiles = models.ManyToManyField(Profile, related_name='sharegroups')

    objects = ShareGroupManager()

    def __str__(self):
        return self.name
class DataSetManager(models.Manager):
    def limit_to_profile(self, profile):
        return super().get_queryset().filter(created_by=profile)

    def shared_with_profile(self, profile):
        return super().get_queryset().filter(
            Q(share_profiles=profile)
            | Q(share_labs__members=profile)
            | Q(share_groups__profiles=profile)
        )

    def accessible_to_profile(self, profile):
        return super().get_queryset().filter(
            Q(created_by=profile)
            | Q(share_profiles=profile)
            | Q(share_labs__members=profile)
            | Q(share_groups__profiles=profile)
        )

        """Writable IF:
        Created by user
        OR accessible by user AND NOT read-only
        """
        return super().get_queryset().filter(
            Q(created_by=profile)
            | (
                (
                    Q(share_profiles=profile)
                    | Q(share_labs__members=profile)
                    | Q(share_groups__profiles=profile)
                ) & Q(read_only=False)
            )
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    """
    DataSets are collections of files for which specific access can be managed.
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    These are akin to folders.
    Access can be granted to whole labs or distinctly to specific users or user groups.
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    """
    name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
    creation_timestamp = models.DateTimeField(verbose_name=_('Creation Timestamp'), auto_now_add=True)
    update_timestamp = models.DateTimeField(verbose_name=_('Last Update Timestamp'), auto_now=True)
    display_fields = HStoreField(null=True, blank=True, verbose_name=_('Display Fields'))
    created_by = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='my_datasets')
    last_update_by = models.ForeignKey(Profile, on_delete=models.PROTECT, verbose_name=_('Last Update By'))
    share_profiles = models.ManyToManyField(Profile, blank=True, related_name='shared_datasets', verbose_name=_('Share with these users'))
    share_labs = models.ManyToManyField(Lab, blank=True, related_name='shared_datasets', verbose_name=_('Share with these labs'))
    share_groups = models.ManyToManyField(ShareGroup, blank=True, related_name='shared_datasets', verbose_name=_('Share with these groups'))
Jonathan Seguin's avatar
Jonathan Seguin committed
    files = models.ManyToManyField(DataFile, related_name='datasets', verbose_name=_('Files'), blank=True)
    read_only = models.BooleanField(default=False, verbose_name=_('Read only'), help_text=_('Prevent edits from users with which you share this DataSet'))
    iric_data_id = models.CharField(max_length=10, verbose_name=_('ID'), null=True)

    objects = DataSetManager()

    def __str__(self):
        return self.name
    def save(self, *args, **kwargs):
        if self.pk is None or self.iric_data_id is None:
            super(DataSet, self).save(*args, **kwargs)
Jonathan Seguin's avatar
Jonathan Seguin committed
            self.iric_data_id = "DS{}".format(hashlib.md5(str(self.id).encode()).hexdigest()[:8]).upper()

        super(DataSet, self).save(*args, **kwargs)

# class LogActionType(models.Model):
#     name = models.CharField(max_length=64, null=False, blank=False, unique=True, primary_key=True, verbose_name=_('Action'))
class Log(models.Model):
    class Meta:
        ordering = ['-ts']

    TYPE_CHOICES = (
        ('add', _('added')),
        ('remove', _('removed')),
        ('add_share', _('shared with')),
        ('remove_share', _('removed sharing with')),
    ts = models.DateTimeField(verbose_name=_('Upload Timestamp'), auto_now_add=True)
    action_type = models.CharField(max_length=64, choices=TYPE_CHOICES, null=False)
    label_en = models.TextField(null=True, blank=True)
    label_fr = models.TextField(null=True, blank=True)
    profile = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='logs')
    obj_dataset = models.ForeignKey(DataSet, null=True, on_delete=models.CASCADE, related_name='logs')
    obj_profile = models.ForeignKey(Profile, null=True, on_delete=models.PROTECT)
    obj_lab = models.ForeignKey(Lab, null=True, on_delete=models.PROTECT)
    obj_group = models.ForeignKey(ShareGroup, null=True, on_delete=models.CASCADE, verbose_name=_('Share with these groups'))
    obj_datafile = models.ForeignKey(DataFile, null=True, on_delete=models.CASCADE)

    @property
    def m2m_obj(self):
        obj = {self.obj_profile, self.obj_lab, self.obj_group, self.obj_datafile}
        obj.remove(None)
        if len(obj) == 1:
            return obj.pop()
        return None
    def __str__(self):
        if self.action_type:
            action = self.get_action_type_display()
                if self.action_type == 'edit':
Jonathan Seguin's avatar
Jonathan Seguin committed
                    return _('{} ago : {} {} "{}"').format(defaultfilters.timesince_filter(self.ts), self.profile.fullname, action, self.label_en if get_language() == 'en' else self.label_fr)

                if self.m2m_obj:
Jonathan Seguin's avatar
Jonathan Seguin committed
                    return _('{} ago : {} {} {} "{}"').format(defaultfilters.timesince_filter(self.ts), self.profile.fullname, action, self.m2m_obj.__class__._meta.verbose_name, self.m2m_obj)

        return super().__str__()

Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
class DataTypes(models.Model):
    """
    DataTypes are a specialization of FSFile for which a set of annotation keys are pre-defined
    Management of DataTypes should be restricted to specialized staff such as Scientific Platforms personnel
    """
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    name = models.CharField(max_length=128, verbose_name=_('Data Type'))
    annotations = HStoreField(verbose_name=_('Suggested Annotations'))
    creation_timestamp = models.DateTimeField(verbose_name=_('Creation Timestamp'), auto_now_add=True)
    update_timestamp = models.DateTimeField(verbose_name=_('Last Update Timestamp'), auto_now=True)

    created_by = models.ForeignKey(Profile, on_delete=models.PROTECT, related_name='my_datatypes')
    last_update_by = models.ForeignKey(Profile, on_delete=models.PROTECT, verbose_name=_('Last Update By'))

    def __str__(self):
        return self.name
class Alert(models.Model):
    message_fr = models.TextField(null=True)
    message_en = models.TextField(null=True)
    start = models.DateTimeField()
    end = models.DateTimeField()

    @property
Jean-Philippe Laverdure's avatar
Jean-Philippe Laverdure committed
    def localized(self):
        if get_language() == 'en':
            return self.message_en
        else:
            return self.message_fr

class AppSettings(models.Model):
    home_institution = models.ForeignKey(Institution, null=True, blank=True, on_delete=models.SET_NULL)