class MeasurementUnit(models.Model): """Defines a unit type and metadata on measurement values.""" class Meta: db_table = "measurement_unit" unit_name = VarCharField( help_text=_("Name for unit of measurement."), unique=True, verbose_name=_("Name"), ) display = models.BooleanField( default=True, help_text=_( "Flag indicating the units should be displayed along with values." ), verbose_name=_("Display"), ) alternate_names = VarCharField( blank=True, help_text=_("Alternative names for the unit."), null=True, verbose_name=_("Alternate Names"), ) type_group = VarCharField( choices=MeasurementType.Group.GROUP_CHOICE, default=MeasurementType.Group.GENERIC, help_text=_("Type of measurement for which this unit is used."), verbose_name=_("Group"), ) # TODO: this should be somehow rolled up into the unit definition conversion_dict = { "g/L": lambda y, metabolite: 1000 * y / metabolite.molar_mass, "mg/L": lambda y, metabolite: y / metabolite.molar_mass, "µg/L": lambda y, metabolite: y / 1000 / metabolite.molar_mass, "Cmol/L": lambda y, metabolite: 1000 * y / metabolite.carbon_count, "mol/L": lambda y, metabolite: 1000 * y, "uM": lambda y, metabolite: y / 1000, "mol/L/hr": lambda y, metabolite: 1000 * y, "mM": lambda y, metabolite: y, } def to_json(self): return {"id": self.pk, "name": self.unit_name} def __str__(self): return self.unit_name
class Institution(models.Model): """An institution to associate with EDD user profiles.""" class Meta: db_table = "profile_institution" institution_name = VarCharField() description = models.TextField(blank=True, null=True) def __str__(self): return self.institution_name
class Category(models.Model): """ Groupings of types of data to load into EDD. Splitting the various file layouts and protocols into higher level groupings allows better navigation for users to select the specific loading process they need. """ class Meta: ordering = ("sort_key", ) verbose_name_plural = "Categories" layouts = models.ManyToManyField( Layout, through="CategoryLayout", help_text=_("Supported input layouts for this load category."), verbose_name=_("File layouts"), related_name="load_category", ) protocols = models.ManyToManyField( edd_models.Protocol, help_text=_("Protocols that appear in this load category."), verbose_name=_("Protocols"), related_name="load_category", ) name = VarCharField(help_text=_("Name of this loading category."), verbose_name=_("Name")) type_group = VarCharField( blank=True, help_text=_( "Constrains measurement types searched during data loading."), null=True, verbose_name=_("Measurement type group"), ) sort_key = models.PositiveIntegerField( null=False, unique=True, help_text=_("Relative order this category is displayed during load."), verbose_name=_("Display order"), ) def __str__(self): return self.name
class MetaboliteSpecies(models.Model): """ Mapping for a metabolite to an species defined by a SBML template. """ class Meta: db_table = "measurement_type_to_species" index_together = ( # index implied by unique, making explicit ("sbml_template", "species"), ) unique_together = ( ("sbml_template", "species"), ("sbml_template", "measurement_type"), ) sbml_template = models.ForeignKey( SBMLTemplate, help_text=_( "The SBML Model defining this species link to a Measurement Type." ), on_delete=models.CASCADE, verbose_name=_("SBML Model"), ) measurement_type = models.ForeignKey( MeasurementType, blank=True, help_text=_("Mesurement type linked to this species in the model."), null=True, on_delete=models.SET_NULL, verbose_name=_("Measurement Type"), ) species = VarCharField( help_text=_("Species name used in the model for this metabolite."), verbose_name=_("Species"), ) short_code = VarCharField( blank=True, default="", help_text=_("Short code used for a species in the model."), null=True, verbose_name=_("Short Code"), ) def __str__(self): return self.species
class MetaboliteExchange(models.Model): """Mapping for a metabolite to an exchange defined by a SBML template.""" class Meta: db_table = "measurement_type_to_exchange" index_together = ( # reactants not unique, but should be searchable ("sbml_template", "reactant_name"), # index implied by unique, making explicit ("sbml_template", "exchange_name"), ) unique_together = ( ("sbml_template", "exchange_name"), ("sbml_template", "measurement_type"), ) sbml_template = models.ForeignKey( SBMLTemplate, help_text=_("The SBML Model containing this exchange reaction."), on_delete=models.CASCADE, verbose_name=_("SBML Model"), ) measurement_type = models.ForeignKey( MeasurementType, blank=True, help_text=_( "Measurement type linked to this exchange reaction in the model."), null=True, on_delete=models.CASCADE, verbose_name=_("Measurement Type"), ) reactant_name = VarCharField( help_text=_("The reactant name used in for this exchange reaction."), verbose_name=_("Reactant Name"), ) exchange_name = VarCharField( help_text=_("The exchange name used in the model."), verbose_name=_("Exchange Name"), ) def __str__(self): return self.exchange_name
class ParserMapping(models.Model): """ Maps incoming layout and MIME to the appropriate Parser class. Represents a mime type-specific parser for a given file layout, e.g. a different parser for each of Excel, CSV for a single file layout. """ class Meta: verbose_name_plural = "Parsers" unique_together = ("layout", "mime_type") layout = models.ForeignKey(Layout, on_delete=models.CASCADE, related_name="parsers") mime_type = VarCharField(help_text=_("Mime type"), verbose_name=_("Mime type")) parser_class = VarCharField(help_text=_("Parser class"), verbose_name=_("Parser")) def create_parser(self, uuid): try: # split fully-qualified class name into module and class names module_name, class_name = self.parser_class.rsplit(sep=".", maxsplit=1) # instantiate the parser. module = importlib.import_module(module_name) parser_class = getattr(module, class_name) return parser_class(uuid) except Exception as e: reporting.raise_errors( uuid, exceptions.BadParserError(details=_( "Unable to instantiate parser class {parser_class}. " "The problem was {problem}").format( parser_class=self.parser_class, problem=str(e))), ) def __str__(self): return f"{self.mime_type}::{self.parser_class}"
class MetadataGroup(models.Model): """Group together types of metadata with a label.""" class Meta: db_table = "metadata_group" group_name = VarCharField( help_text=_("Name of the group/class of metadata."), unique=True, verbose_name=_("Group Name"), ) def __str__(self): return self.group_name
class Datasource(models.Model): """ Defines an outside source for bits of data in the system. Initially developed to track where basic metabolite information originated (e.g. BIGG, KEGG, manual input). """ name = VarCharField( help_text=_("The source used for information on a measurement type."), verbose_name=_("Datasource"), ) url = VarCharField(blank=True, default="", help_text=_("URL of the source."), verbose_name=_("URL")) download_date = models.DateField( auto_now=True, help_text=_("Date when information was accessed and copied."), verbose_name=_("Download Date"), ) created = models.ForeignKey( Update, editable=False, help_text=_("Update object logging the creation of this Datasource."), on_delete=models.PROTECT, related_name="datasource", verbose_name=_("Created"), ) def __str__(self): return f"{self.name} <{self.url}>" def save(self, *args, **kwargs): if self.created_id is None: update = kwargs.get("update", None) if update is None: update = Update.load_update() self.created = update super().save(*args, **kwargs)
class MeasurementNameTransform(models.Model): class Meta: db_table = "measurement_name_transform" input_type_name = VarCharField( help_text=_("Name of this Measurement Type in input."), verbose_name=_("Input Measurement Type"), ) edd_type_name = models.ForeignKey( edd_models.MeasurementType, on_delete=models.deletion.CASCADE, verbose_name=_("EDD Type Name"), ) parser = VarCharField(blank=True, null=True) def to_json(self): return { "id": self.pk, "input_type_name": self.input_type_name, "edd_type_name": self.edd_type_name.type_name, "parser": self.parser, }
class Layout(models.Model): """ Represents an input file layout for EDD imports. Having a DB model for this data allows different EDD deployments to add in custom parsers and configure them via the admin app. """ name = VarCharField(help_text=_("Name of this file layout."), verbose_name=_("Name")) description = models.TextField( blank=True, help_text=_("Description of this object."), null=True, verbose_name=_("Description"), ) def __str__(self): return self.name
class CampaignMembership(models.Model): """A link between a Campaign and Study.""" class Status: ACTIVE = "a" COMPLETE = "c" ABANDONED = "z" CHOICE = ( (ACTIVE, _("Active")), (COMPLETE, _("Complete")), (ABANDONED, _("Abandoned")), ) campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE) study = models.ForeignKey(edd_models.Study, on_delete=models.CASCADE) status = VarCharField( choices=Status.CHOICE, default=Status.ACTIVE, help_text=_("Status of a Study in the linked Campaign."), )
class UserProfile(models.Model): """Additional profile information on a user.""" class Meta: db_table = "profile_user" user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) initials = VarCharField(blank=True, null=True) description = models.TextField(blank=True, null=True) institutions = models.ManyToManyField(Institution, through="InstitutionID") preferences = models.JSONField(blank=True, default=dict) approved = models.BooleanField( default=False, help_text=_( "Flag showing if this account has been approved for login."), verbose_name=_("Approved"), ) def __str__(self): return str(self.user)
class InstitutionID(models.Model): """ A link to an Institution with an (optional) identifier; e.g. JBEI with LBL employee ID number. """ class Meta: constraints = [ models.UniqueConstraint(fields=["profile", "sort_key"], name="profile_institution_ordering_idx"), ] db_table = "profile_institution_user" institution = models.ForeignKey(Institution, on_delete=models.CASCADE) profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE) identifier = VarCharField(blank=True, null=True) sort_key = models.PositiveIntegerField( null=False, help_text=_( "Relative order this Institution is displayed in a UserProfile."), verbose_name=_("Display order"), )
class StudyPermission(Permission, models.Model): """ Access given for a *specific* study instance, rather than for object types provided by Django. """ class Meta: abstract = True study = models.ForeignKey( "main.Study", help_text=_("Study this permission applies to."), on_delete=models.CASCADE, verbose_name=_("Study"), ) permission_type = VarCharField( choices=Permission.TYPE_CHOICE, default=Permission.NONE, help_text=_("Type of permission."), verbose_name=_("Permission"), ) def get_type_label(self): return dict(self.TYPE_CHOICE).get(self.permission_type, "?") def is_read(self): """ Test if the permission grants read privileges. :returns: True if permission grants read access """ return self.permission_type in self.CAN_VIEW def is_write(self): """ Test if the permission grants write privileges. :returns: True if permission grants write access """ return self.permission_type in self.CAN_EDIT
class DefaultUnit(models.Model): class Meta: db_table = "default_unit" measurement_type = models.ForeignKey( edd_models.MeasurementType, on_delete=models.deletion.CASCADE, verbose_name=_("Measurement Type"), ) unit = models.ForeignKey(edd_models.MeasurementUnit, on_delete=models.deletion.CASCADE) protocol = models.ForeignKey(edd_models.Protocol, blank=True, null=True, on_delete=models.deletion.CASCADE) parser = VarCharField(blank=True, null=True) def to_json(self): return { "id": self.pk, "type_name": self.measurement_type.type_name, "unit_name": self.unit.unit_name, }
class EDDObject(EDDMetadata, EDDSerialize): """A first-class EDD object, with update trail, comments, attachments.""" class Meta: db_table = "edd_object" objects = EDDObjectManager() name = VarCharField(help_text=_("Name of this object."), verbose_name=_("Name")) description = models.TextField( blank=True, help_text=_("Description of this object."), null=True, verbose_name=_("Description"), ) active = models.BooleanField( default=True, help_text=_("Flag showing if this object is active and displayed."), verbose_name=_("Active"), ) updates = models.ManyToManyField( Update, db_table="edd_object_update", help_text=_("List of Update objects logging changes to this object."), related_name="+", verbose_name=_("Updates"), ) # these are used often enough we should save extra queries by including as fields created = models.ForeignKey( Update, editable=False, help_text=_("Update used to create this object."), on_delete=models.PROTECT, related_name="object_created", verbose_name=_("Created"), ) updated = models.ForeignKey( Update, editable=False, help_text=_("Update used to last modify this object."), on_delete=models.PROTECT, related_name="object_updated", verbose_name=_("Last Modified"), ) # linking together EDD instances will be easier later if we define UUIDs now uuid = models.UUIDField( editable=False, help_text=_("Unique identifier for this object."), unique=True, verbose_name=_("UUID"), ) @property def mod_epoch(self): return arrow.get(self.updated.mod_time).int_timestamp @property def last_modified(self): return self.updated.format_timestamp() def was_modified(self): return self.updates.count() > 1 @property def date_created(self): return self.created.format_timestamp() def get_attachment_count(self): if hasattr(self, "_file_count"): return self._file_count return self.files.count() @property def attachments(self): return self.files.all() @property def comment_list(self): return self.comments.order_by("created__mod_time").all() def get_comment_count(self): if hasattr(self, "_comment_count"): return self._comment_count return self.comments.count() @classmethod def metadata_type_frequencies(cls): return dict( # TODO: do this with Django model APIs instead of raw SQL MetadataType.objects.extra( select={ "count": "SELECT COUNT(1) FROM edd_object o " f"INNER JOIN {cls._meta.db_table} x ON o.id = x.object_ref_id " "WHERE o.metadata ? metadata_type.id::varchar" }).values_list("id", "count")) def __str__(self): return self.name @classmethod def export_columns(cls, table_generator, instances=None): # define column for object ID table_generator.define_field_column(cls._meta.get_field("id"), heading=f"{cls.__name__} ID") # define column for object name table_generator.define_field_column(cls._meta.get_field("name"), heading=f"{cls.__name__} Name") def to_json(self, depth=0): return { "id": self.pk, "name": self.name, "description": self.description, "active": self.active, "meta": self.metadata, # Always include expanded created/updated objects instead of IDs "modified": self.updated.to_json(depth) if self.updated else None, "created": self.created.to_json(depth) if self.created else None, } def to_json_str(self, depth=0): """ Used in overview.html. Serializing directly in the template creates strings like "u'description'" that Javascript can't parse. """ json_dict = self.to_json(depth) return json.dumps(json_dict, ensure_ascii=False).encode("utf8") def user_can_read(self, user): return True def user_can_write(self, user): return user and user.is_superuser
class Measurement(EDDMetadata, EDDSerialize): """A plot of data points for an (assay, measurement type) pair.""" class Meta: db_table = "measurement" class Compartment: """ Enumeration of localized compartments applying to the measurement. UNKNOWN = default; no specific localization INTRACELLULAR = measurement inside of a cell, in cytosol EXTRACELLULAR = measurement outside of a cell """ UNKNOWN = "0" INTRACELLULAR = "1" EXTRACELLULAR = "2" namecode = namedtuple("namecode", ("name", "code")) names = { UNKNOWN: namecode(_("N/A"), _("")), INTRACELLULAR: namecode(_("Intracellular/Cytosol (Cy)"), _("IC")), EXTRACELLULAR: namecode(_("Extracellular"), _("EC")), } CHOICE = tuple((k, v.name) for k, v in names.items()) @classmethod def to_json(cls): return {k: {"id": k, **v._asdict()} for k, v in cls.names.items()} class Format: """ Enumeration of formats measurement values can take. SCALAR = single timepoint X value, single measurement Y value (one item array) VECTOR = single timepoint X value, vector measurement Y value (mass-distribution, index by labeled carbon count; interpret each value as ratio with sum of all values) HISTOGRAM_NAIVE = single timepoint X value, vector measurement Y value (bins with counts of population measured within bin value, bin size/range set via y_units) SIGMA = single timepoint X value, 3-item-list Y value (average, variance, sample size) RANGE = single timepoint X value, 3-item-list Y value (best, hi, lo) VECTOR_RANGE = single timepoint X value, 3n vector Y value (mass-distribution, n best values first, n hi values, n lo values, index by xn + labeled carbon count) PACKED = series of scalar values packed into a single pair of value vectors HISTOGRAM = timepoint plus n+1 X values, vector of n Y values per bin HISTOGRAM_STEP = timepoint, start, step X values, vector Y values per bin """ SCALAR = "0" VECTOR = "1" HISTOGRAM_NAIVE = "2" SIGMA = "3" RANGE = "4" VECTOR_RANGE = "5" PACKED = "6" HISTOGRAM = "7" HISTOGRAM_STEP = "8" names = { SCALAR: _("scalar"), VECTOR: _("vector"), HISTOGRAM_NAIVE: _("histogram (deprecated)"), SIGMA: _("sigma"), RANGE: _("range"), VECTOR_RANGE: _("vector range"), PACKED: _("packed"), HISTOGRAM: _("histogram"), HISTOGRAM_STEP: _("stepped histogram"), } CHOICE = tuple(names.items()) study = models.ForeignKey( Study, help_text=_("The Study containing this Measurement."), on_delete=models.CASCADE, verbose_name=_("Study"), ) assay = models.ForeignKey( Assay, help_text=_("The Assay creating this Measurement."), on_delete=models.CASCADE, verbose_name=_("Assay"), ) experimenter = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, help_text= _("EDD User that set up the experimental conditions of this Measurement." ), null=True, on_delete=models.PROTECT, related_name="measurement_experimenter_set", verbose_name=_("Experimenter"), ) measurement_type = models.ForeignKey( MeasurementType, help_text=_("The type of item measured for this Measurement."), on_delete=models.PROTECT, verbose_name=_("Type"), ) x_units = models.ForeignKey( MeasurementUnit, help_text=_("The units of the X-axis for this Measurement."), on_delete=models.PROTECT, related_name="+", verbose_name=_("X Units"), ) y_units = models.ForeignKey( MeasurementUnit, help_text=_("The units of the Y-axis for this Measurement."), on_delete=models.PROTECT, related_name="+", verbose_name=_("Y Units"), ) update_ref = models.ForeignKey( Update, help_text=_("The Update triggering the setting of this Measurement."), on_delete=models.PROTECT, verbose_name=_("Updated"), ) active = models.BooleanField( default=True, help_text= _("Flag indicating this Measurement is active and should be displayed." ), verbose_name=_("Active"), ) compartment = VarCharField( choices=Compartment.CHOICE, default=Compartment.UNKNOWN, help_text=_("Compartment of the cell for this Measurement."), verbose_name=_("Compartment"), ) measurement_format = VarCharField( choices=Format.CHOICE, default=Format.SCALAR, help_text=_("Enumeration of value formats for this Measurement."), verbose_name=_("Format"), ) @classmethod def export_columns(cls, table_generator, instances=None): table_generator.define_field_column( cls._meta.get_field("measurement_type"), lookup=lambda measure: measure.measurement_type.export_name(), ) table_generator.define_field_column( cls._meta.get_field("measurement_type"), heading=_("Formal Type ID"), key="formal_id", lookup=measurement_formal_id, ) table_generator.define_field_column( cls._meta.get_field("update_ref"), heading=_("Measurement Updated"), lookup=lambda measure: measure.update_ref.mod_time, ) table_generator.define_field_column( cls._meta.get_field("x_units"), lookup=measurement_x_unit, ) table_generator.define_field_column( cls._meta.get_field("y_units"), lookup=measurement_y_unit, ) def to_json(self, depth=0): return { "id": self.pk, "assay": self.get_attr_depth("assay", depth), "type": self.get_attr_depth("measurement_type", depth), "comp": self.compartment, "format": self.measurement_format, # including points here is extremely inefficient # better to directly filter MeasurementValue and map to parent IDs later # "values": map(lambda p: p.to_json(), self.measurementvalue_set.all()), "x_units": self.x_units_id, "y_units": self.y_units_id, "meta": self.metadata, } def __str__(self): return f"Measurement[{self.assay_id}][{self.measurement_type}]" # may not be the best method name, if we ever want to support other # types of data as vectors in the future def is_carbon_ratio(self): return self.measurement_format == Measurement.Format.VECTOR def valid_data(self): """Data for which the y-value is defined (non-NULL, non-blank).""" mdata = list(self.data()) return [md for md in mdata if md.is_defined()] def is_extracellular(self): return self.compartment == Measurement.Compartment.EXTRACELLULAR def data(self): """Return the data associated with this measurement.""" return self.measurementvalue_set.all() @property def name(self): """alias for self.measurement_type.type_name""" return self.measurement_type.type_name @property def compartment_symbol(self): return Measurement.Compartment.short_names[int(self.compartment)] @property def full_name(self): """measurement compartment plus measurement_type.type_name""" lookup = dict(Measurement.Compartment.CHOICE) return (lookup.get(self.compartment) + " " + self.name).strip() # TODO also handle vectors def extract_data_xvalues(self, defined_only=False): qs = self.measurementvalue_set.order_by("x") if defined_only: qs = qs.exclude(Q(y=None) | Q(y__len=0)) # first index unpacks single value from tuple # second index unpacks first value from X return [x[0][0] for x in qs.values_list("x")] # this shouldn't need to handle vectors def interpolate_at(self, x): if self.measurement_format != Measurement.Format.SCALAR: raise ValueError("Can only interpolate scalar values") from main.utilities import interpolate_at return interpolate_at(self.valid_data(), x) @property def y_axis_units_name(self): """ Human-readable units for Y-axis. Not intended for repeated/bulk use, since it involves a foreign key lookup. """ return self.y_units.unit_name def is_concentration_measurement(self): return self.y_axis_units_name in [ "mg/L", "g/L", "mol/L", "mM", "uM", "Cmol/L" ] @classmethod def active_in(cls, *, study_id, protocol_id, assay_id=None): """ Queries all active/enabled measurements matching criteria. """ assay_filter = Q() if assay_id is None else Q(assay_id=assay_id) active = cls.objects.filter( assay_filter, active=True, assay__active=True, assay__line__active=True, assay__line__study_id=study_id, assay__protocol_id=protocol_id, ) return active
class MetadataType(models.Model, EDDSerialize): """Type information for arbitrary key-value data stored on EDDObject instances.""" # defining values to use in the for_context field STUDY = "S" LINE = "L" ASSAY = "A" CONTEXT_SET = ((STUDY, _("Study")), (LINE, _("Line")), (ASSAY, _("Assay"))) # pre-defined values that should always exist in the system _SYSTEM_TYPES = ( # type_field metadata to map to Model object fields Metadata( for_context=ASSAY, input_type="textarea", type_field="description", type_i18n="main.models.Assay.description", type_name="Assay Description", uuid="4929a6ad-370c-48c6-941f-6cd154162315", ), Metadata( for_context=ASSAY, input_type="user", type_field="experimenter", type_i18n="main.models.Assay.experimenter", type_name="Assay Experimenter", uuid="15105bee-e9f1-4290-92b2-d7fdcb3ad68d", ), Metadata( for_context=ASSAY, input_type="string", type_field="name", type_i18n="main.models.Assay.name", type_name="Assay Name", uuid="33125862-66b2-4d22-8966-282eb7142a45", ), Metadata( for_context=LINE, input_type="carbon_source", type_field="carbon_source", type_i18n="main.models.Line.carbon_source", type_name="Carbon Source(s)", uuid="4ddaf92a-1623-4c30-aa61-4f7407acfacc", ), Metadata( for_context=LINE, input_type="checkbox", type_field="control", type_i18n="main.models.Line.control", type_name="Control", uuid="8aa26735-e184-4dcd-8dd1-830ec240f9e1", ), Metadata( for_context=LINE, input_type="user", type_field="contact", type_i18n="main.models.Line.contact", type_name="Line Contact", uuid="13672c8a-2a36-43ed-928f-7d63a1a4bd51", ), Metadata( for_context=LINE, input_type="textarea", type_field="description", type_i18n="main.models.Line.description", type_name="Line Description", uuid="5fe84549-9a97-47d2-a897-8c18dd8fd34a", ), Metadata( for_context=LINE, input_type="user", type_field="experimenter", type_i18n="main.models.Line.experimenter", type_name="Line Experimenter", uuid="974c3367-f0c5-461d-bd85-37c1a269d49e", ), Metadata( for_context=LINE, input_type="string", type_field="name", type_i18n="main.models.Line.name", type_name="Line Name", uuid="b388bcaa-d14b-4d7f-945e-a6fcb60142f2", ), Metadata( for_context=LINE, input_type="strain", type_field="strains", type_i18n="main.models.Line.strains", type_name="Strain(s)", uuid="292f1ca7-30de-4ba1-89cd-87d2f6291416", ), # "true" metadata, but directly referenced by code for specific purposes Metadata( default_value="--", for_context=LINE, input_type="media", type_i18n="main.models.Line.Media", type_name="Media", uuid="463546e4-a67e-4471-a278-9464e78dbc9d", ), Metadata( for_context=ASSAY, # TODO: consider making this: input_type="readonly" input_type="string", type_i18n="main.models.Assay.original", type_name="Original Name", uuid="5ef6500e-0f8b-4eef-a6bd-075bcb655caa", ), Metadata( for_context=LINE, input_type="replicate", type_i18n="main.models.Line.replicate", type_name="Replicate", uuid="71f5cd94-4dd4-45ca-a926-9f0717631799", ), Metadata( for_context=ASSAY, input_type="time", type_i18n="main.models.Assay.Time", type_name="Time", uuid="6629231d-4ef0-48e3-a21e-df8db6dfbb72", ), ) _SYSTEM_DEF = {t.type_name: t for t in _SYSTEM_TYPES} SYSTEM = {t.type_name: t.uuid for t in _SYSTEM_TYPES} class Meta: db_table = "metadata_type" unique_together = (("type_name", "for_context"),) # optionally link several metadata types into a common group group = models.ForeignKey( MetadataGroup, blank=True, help_text=_("Group for this Metadata Type"), null=True, on_delete=models.PROTECT, verbose_name=_("Group"), ) # a default label for the type; should normally use i18n lookup for display type_name = VarCharField( help_text=_("Name for Metadata Type"), verbose_name=_("Name") ) # an i18n lookup for type label type_i18n = VarCharField( blank=True, help_text=_("i18n key used for naming this Metadata Type."), null=True, verbose_name=_("i18n Key"), ) # field to store metadata, or None if stored in metadata type_field = VarCharField( blank=True, default=None, help_text=_( "Model field where metadata is stored; blank stores in metadata dictionary." ), null=True, verbose_name=_("Field Name"), ) # type of the input on front-end; support checkboxes, autocompletes, etc # blank/null falls back to plain text input field input_type = VarCharField( blank=True, help_text=_("Type of input fields for values of this Metadata Type."), null=True, verbose_name=_("Input Type"), ) # a default value to use if the field is left blank default_value = VarCharField( blank=True, help_text=_("Default value for this Metadata Type."), verbose_name=_("Default Value"), ) # label used to prefix values prefix = VarCharField( blank=True, help_text=_("Prefix text appearing before values of this Metadata Type."), verbose_name=_("Prefix"), ) # label used to postfix values (e.g. unit specifier) postfix = VarCharField( blank=True, help_text=_("Postfix text appearing after values of this Metadata Type."), verbose_name=_("Postfix"), ) # target object for metadata for_context = VarCharField( choices=CONTEXT_SET, help_text=_("Type of EDD Object this Metadata Type may be added to."), verbose_name=_("Context"), ) # linking together EDD instances will be easier later if we define UUIDs now uuid = models.UUIDField( editable=False, help_text=_("Unique identifier for this Metadata Type."), unique=True, verbose_name=_("UUID"), ) @classmethod def all_types_on_instances(cls, instances): # grab all the keys on each instance metadata all_ids = [ set(o.metadata.keys()) for o in instances if isinstance(o, EDDMetadata) ] # reduce all into a set to get only unique ids ids = set().union(*all_ids) return MetadataType.objects.filter(pk__in=ids).order_by( Func(F("type_name"), function="LOWER") ) @classmethod def system(cls, name): """Load a pre-defined system-wide MetadataType.""" typedef = cls._SYSTEM_DEF.get(name, None) if typedef is None: raise cls.DoesNotExist fields = {f.name for f in dataclasses.fields(Metadata)} defaults = {k: v for k, v in typedef.__dict__.items() if k in fields and v} meta, created = cls.objects.get_or_create(uuid=typedef.uuid, defaults=defaults) return meta def decode_value(self, value): """ Default MetadataType class reflects back the passed value loaded from JSON. Subclasses may try to modify the value to convert to arbitrary Python values instead of a JSON-compatible dict. """ return value def encode_value(self, value): """ Default MetadataType class reflects back the passed value to send to JSON. Subclasses may try to modify the value to serialize arbitrary Python values to a JSON-compatible value. """ return value def for_line(self): return self.for_context == self.LINE def for_assay(self): return self.for_context == self.ASSAY def for_study(self): return self.for_context == self.STUDY def __str__(self): return self.type_name def to_json(self, depth=0): return { "id": self.pk, "name": self.type_name, "i18n": self.type_i18n, "input_type": self.input_type, "prefix": self.prefix, "postfix": self.postfix, "default": self.default_value, "context": self.for_context, }
class ProteinIdentifier(MeasurementType): """Defines additional metadata on proteomic measurement type.""" class Meta: db_table = "protein_identifier" # protein names use: # type_name = "human-readable" name; e.g. AATM_RABIT # accession_code = accession code ID portion; e.g. P12345 # accession_id = "full" accession ID if available; e.g. sp|P12345|AATM_RABIT # if "full" version unavailable, repeat the accession_code accession_id = VarCharField( blank=True, help_text=_("Accession ID for protein characterized in e.g. UniProt."), null=True, verbose_name=_("Accession ID"), ) accession_code = VarCharField( blank=True, help_text=_("Required portion of Accession ID for easier lookup."), null=True, verbose_name=_("Accession Code"), ) length = models.IntegerField(blank=True, help_text=_("sequence length"), null=True, verbose_name=_("Length")) mass = models.DecimalField( blank=True, decimal_places=5, help_text=_("of unprocessed protein, in Daltons"), max_digits=16, null=True, verbose_name=_("Mass"), ) # TODO find how this can also match JGI accession IDs accession_pattern = re.compile( # optional identifier for SwissProt or TrEMBL r"(?:[a-z]{2}\|)?" # the ID r"([OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9](?:[A-Z][A-Z0-9]{2}[0-9]){1,2})" # optional name r"(?:\|(\w+))?") def export_name(self): if self.accession_id: return self.accession_id return self.type_name def to_solr_json(self): """ Convert the MeasurementType model to a dict structure formatted for Solr JSON. """ return dict(super().to_solr_json(), **{ "p_length": self.length, "p_mass": self.mass }) def update_from_uniprot(self): match = self.accession_pattern.match(self.accession_id) if match: uniprot_id = match.group(1) for name, value in self._load_uniprot_values(uniprot_id): setattr(self, name, value) if not self.provisional: self.save() @classmethod def _get_or_create_from_uniprot(cls, uniprot_id, accession_id): try: protein = cls.objects.get(accession_code=uniprot_id) except cls.DoesNotExist: url = cls._uniprot_url(uniprot_id) protein = cls.objects.create( accession_code=uniprot_id, accession_id=accession_id, type_source=Datasource.objects.create(name="UniProt", url=url), ) return protein @classmethod def _load_uniprot(cls, uniprot_id, accession_id): try: protein = cls._get_or_create_from_uniprot(uniprot_id, accession_id) lookup_protein_in_uniprot.delay(protein.id) return protein except Exception: logger.exception(f"Failed to create from UniProt {uniprot_id}") raise ValidationError( _u("Could not create Protein from {uniprot_id}").format( uniprot_id=uniprot_id)) @classmethod def _load_uniprot_values(cls, uniprot_id): url = cls._uniprot_url(uniprot_id) values = {} # define some RDF predicate terms mass_predicate = URIRef("http://purl.uniprot.org/core/mass") sequence_predicate = URIRef("http://purl.uniprot.org/core/sequence") value_predicate = URIRef( "http://www.w3.org/1999/02/22-rdf-syntax-ns#value") # build the RDF graph try: graph = Graph() graph.parse(url) # find top-level references subject = URIRef(f"http://purl.uniprot.org/uniprot/{uniprot_id}") isoform = graph.value(subject, sequence_predicate) # find values of interest values.update( type_name=cls._uniprot_name(graph, subject, uniprot_id)) sequence = graph.value(isoform, value_predicate) if sequence: values.update(length=len(sequence.value)) mass = graph.value(isoform, mass_predicate) if mass: values.update(mass=mass.value) values.update(provisional=False) except Exception: logger.exception(f"Failed to read UniProt: {uniprot_id}") values.update(provisional=True) return values @classmethod def _uniprot_name(cls, graph, subject, uniprot_id): """ Parses the RDF for name using ordered preferences: recommendedName, then submittedName, then mnemonic, then uniprot_id. """ fullname_predicate = URIRef("http://purl.uniprot.org/core/fullName") mnemonic_predicate = URIRef("http://purl.uniprot.org/core/mnemonic") recname_predicate = URIRef( "http://purl.uniprot.org/core/recommendedName") subname_predicate = URIRef( "http://purl.uniprot.org/core/submittedName") names = [ # get the fullName value of the recommendedName graph.value(graph.value(subject, recname_predicate), fullname_predicate), # get the fullName value of the submittedName graph.value(graph.value(subject, subname_predicate), fullname_predicate), # get the literal value of the mnemonic getattr(graph.value(subject, mnemonic_predicate), "value", None), ] # fallback to uniprot_id if all above are None return next((name for name in names if name is not None), uniprot_id) @classmethod def _uniprot_url(cls, uniprot_id): return f"http://www.uniprot.org/uniprot/{uniprot_id}.rdf" @classmethod def _load_ice(cls, link): part = link.strain.part datasource = Datasource.objects.create(name="Part Registry", url=link.strain.registry_url) protein = cls.objects.create( type_name=link.strain.name, type_source=datasource, accession_id=part.part_id, ) link.protein = protein link.save() return protein @classmethod def load_or_create(cls, protein_name, user): # extract Uniprot accession data from the measurement name, if present accession_match = cls.accession_pattern.match(protein_name) proteins = cls.objects.none() if accession_match: accession_code = accession_match.group(1) proteins = cls.objects.filter(accession_code=accession_code) else: proteins = cls.objects.filter(accession_code=protein_name) # force query to LIMIT 2, anything more than one is treated same proteins = proteins[:2] if len(proteins) > 1: # fail if protein couldn't be uniquely matched raise ValidationError( _u('More than one match was found for protein name "{type_name}".' ).format(type_name=protein_name)) elif len(proteins) == 0: # try to create a new protein link = ProteinStrainLink() if accession_match: # if it looks like a UniProt ID, look up in UniProt accession_code = accession_match.group(1) return cls._load_uniprot(accession_code, protein_name) elif link.check_ice(user.email, protein_name): # if it is found in ICE, create based on ICE info return cls._load_ice(link) elif getattr(settings, "REQUIRE_UNIPROT_ACCESSION_IDS", True): raise ValidationError( _u('Protein name "{type_name}" is not a valid UniProt accession id.' ).format(type_name=protein_name)) logger.info(f"Creating a new ProteinIdentifier for {protein_name}") # not requiring accession ID or ICE entry; just create protein with arbitrary name datasource = Datasource.objects.create(name=user.username, url=user.email) return cls.objects.create( type_name=protein_name, provisional=True, accession_code=protein_name, accession_id=protein_name, type_source=datasource, ) return proteins[0] @classmethod def match_accession_id(cls, text): """ Tests whether the input text matches the pattern of a Uniprot accession id, and if so, extracts & returns the required identifier portion of the text, less optional prefix/suffix allowed by the pattern. :param text: the text to match :return: the Uniprot identifier if the input text matched the accession id pattern, or the entire input string if not """ match = cls.accession_pattern.match(text) if match: return match.group(1) return text def __str__(self): return self.type_name def save(self, *args, **kwargs): # force PROTEINID group self.type_group = MeasurementType.Group.PROTEINID super().save(*args, **kwargs)
class MeasurementType(EDDSerialize, models.Model): """ Defines the type of measurement being made. A generic measurement only has name and short name; if the type is a metabolite, the metabolite attribute will contain additional metabolite info. """ class Meta: db_table = "measurement_type" class Group: """ Note that when a new group type is added here, code will need to be updated elsewhere, including the Javascript/Typescript front end. Look for the string 'MeasurementGroupCode' in comments. """ GENERIC = "_" METABOLITE = "m" GENEID = "g" PROTEINID = "p" PHOSPHOR = "h" GROUP_CHOICE = ( (GENERIC, _("Generic")), (METABOLITE, _("Metabolite")), (GENEID, _("Gene Identifier")), (PROTEINID, _("Protein Identifier")), (PHOSPHOR, _("Phosphor")), ) type_name = VarCharField( help_text=_("Name of this Measurement Type."), verbose_name=_("Measurement Type"), ) short_name = VarCharField( blank=True, help_text=_("(DEPRECATED) Short name used in SBML output."), null=True, verbose_name=_("Short Name"), ) type_group = VarCharField( choices=Group.GROUP_CHOICE, default=Group.GENERIC, help_text=_("Class of data for this Measurement Type."), verbose_name=_("Type Group"), ) type_source = models.ForeignKey( Datasource, blank=True, help_text=_( "Datasource used for characterizing this Measurement Type."), null=True, on_delete=models.PROTECT, verbose_name=_("Datasource"), ) provisional = models.BooleanField( default=False, help_text= _("Flag indicating if the type is pending lookup in external Datasource" ), verbose_name=_("Provisional"), ) # linking together EDD instances will be easier later if we define UUIDs now uuid = models.UUIDField( editable=False, help_text=_("Unique ID for this Measurement Type."), unique=True, verbose_name=_("UUID"), ) alt_names = ArrayField( VarCharField(), blank=True, default=list, help_text=_("Alternate names for this Measurement Type."), verbose_name=_("Synonyms"), ) def save(self, *args, **kwargs): if self.uuid is None: self.uuid = uuid4() super().save(*args, **kwargs) def to_solr_value(self): return f"{self.pk}@{self.type_name}" def to_solr_json(self): """ Convert the MeasurementType model to a dict structure formatted for Solr JSON. """ source_name = None # Check if this is coming from a child MeasurementType, and ref the base type mtype = getattr(self, "measurementtype_ptr", None) # check for annotated source attribute on self and base type if hasattr(self, "_source_name"): source_name = self._source_name elif mtype and hasattr(mtype, "_source_name"): source_name = mtype._source_name elif self.type_source: source_name = self.type_source.name return { "id": self.id, "uuid": self.uuid, "name": self.type_name, "family": self.type_group, # use the annotated attr if present, otherwise must make a new query "source": source_name, } def to_json(self, depth=0): payload = { "id": self.pk, "uuid": self.uuid, "name": self.type_name, "family": self.type_group, } # optionally add CID or Accession if from annotated query if (cid := getattr(self, "cid", None)) is not None: payload["cid"] = cid elif (accession := getattr(self, "accession", None)) is not None: payload["accession"] = accession
class WorklistColumn(models.Model): """Defines metadata defaults and layout.""" class Meta: constraints = (models.constraints.UniqueConstraint( condition=models.Q(ordering__isnull=False), fields=("ordering", "template"), name="unique_column_ordering", ), ) db_table = "worklist_column" template = models.ForeignKey( WorklistTemplate, help_text=_("Parent Worklist Template for this column."), on_delete=models.CASCADE, verbose_name=_("Template"), ) # if meta_type is None, treat default_value as format string meta_type = models.ForeignKey( metadata.MetadataType, blank=True, help_text=_("Type of Metadata in this column."), null=True, on_delete=models.PROTECT, verbose_name=_("Metadata Type"), ) # if None, default to meta_type.type_name or '' heading = VarCharField( blank=True, help_text=_("Column header text."), null=True, verbose_name=_("Heading"), ) # potentially override the default value in templates? default_value = VarCharField( blank=True, help_text=_("Default value for this column."), null=True, verbose_name=_("Default Value"), ) # text to display in UI explaining how to modify column help_text = models.TextField( blank=True, help_text=_( "UI text to display explaining how to modify this column."), null=True, verbose_name=_("Help Text"), ) # allow ordering of metadata ordering = models.IntegerField( blank=True, help_text=_("Order this column will appear in worklist export."), null=True, verbose_name=_("Ordering"), ) def get_default(self): if self.default_value: return self.default_value elif self.meta_type: return self.meta_type.default_value return "" def get_format_dict(self, instance, *args, **kwargs): """ Build dict used in format string for columns that use it. This implementation re-uses EDDObject.to_json(), in a flattened format. """ fmt_dict = flatten_json(instance.to_json(depth=1) if instance else {}) # add in: date # TODO: pass in tz based on user profile? fmt_dict.update(today=arrow.now().format("YYYYMMDD")) fmt_dict.update(**kwargs) return fmt_dict def __str__(self): if self.heading: return self.heading return str(self.meta_type)
class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("main", "0001_edd_2_7"), ] operations = [ migrations.CreateModel( name="StudyLog", fields=[ ( "id", models.UUIDField( default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, ), ), ( "detail", models.JSONField( blank=True, decoder=JSONDecoder, default=dict, encoder=JSONEncoder, editable=False, help_text= "JSON structure with extra details specific to the event.", verbose_name="Details", ), ), ( "event", VarCharField( choices=[ ("STUDY_CREATED", "Study Created"), ("STUDY_DESCRIBED", "Study Described"), ("STUDY_EXPORTED", "Study Exported"), ("STUDY_IMPORTED", "Study Imported"), ("STUDY_PERMISSION", "Study Permission Changed"), ("STUDY_VIEWED", "Study Viewed"), ("STUDY_WORKLIST", "Study Worklist"), ], editable=False, help_text="Type of logged metric event.", verbose_name="Event", ), ), ( "timestamp", models.DateTimeField( auto_now_add=True, help_text="Timestamp of the logged metric event.", verbose_name="Timestamp", ), ), ( "study", models.ForeignKey( blank=True, editable=False, help_text= "The Study associated with the logged metric event.", null=True, on_delete=models.deletion.SET_NULL, related_name="metric_log", to="main.study", verbose_name="Study", ), ), ( "user", models.ForeignKey( editable=False, help_text= "The user triggering the logged metric event.", null=True, on_delete=models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name="User", ), ), ], options={ "verbose_name": "Study Log", "verbose_name_plural": "Study Logs" }, ), ]
class Attachment(models.Model): """ File uploads attached to an EDDObject; include MIME, file name, and description. """ class Meta: db_table = "attachment" object_ref = models.ForeignKey("EDDObject", on_delete=models.CASCADE, related_name="files") file = FileField( help_text=_("Path to file data."), max_length=None, upload_to="%Y/%m/%d", verbose_name=_("File Path"), ) filename = VarCharField(help_text=_("Name of attachment file."), verbose_name=_("File Name")) created = models.ForeignKey( Update, help_text=_("Update used to create the attachment."), on_delete=models.PROTECT, verbose_name=_("Created"), ) description = models.TextField( blank=True, help_text=_("Description of attachment file contents."), null=False, verbose_name=_("Description"), ) mime_type = VarCharField( blank=True, help_text=_("MIME ContentType of the attachment."), null=True, verbose_name=_("MIME"), ) file_size = models.IntegerField( default=0, help_text=_("Total byte size of the attachment."), verbose_name=_("Size"), ) extensions_to_icons = defaultdict( lambda: "icon-generic.png", { ".zip": "icon-zip.png", ".gzip": "icon-zip.png", ".bzip": "icon-zip.png", ".gz": "icon-zip.png", ".dmg": "icon-zip.png", ".rar": "icon-zip.png", ".ico": "icon-image.gif", ".gif": "icon-image.gif", ".jpg": "icon-image.gif", ".jpeg": "icon-image.gif", ".png": "icon-image.gif", ".tif": "icon-image.gif", ".tiff": "icon-image.gif", ".psd": "icon-image.gif", ".svg": "icon-image.gif", ".mov": "icon-video.png", ".avi": "icon-video.png", ".mkv": "icon-video.png", ".txt": "icon-text.png", ".rtf": "icon-text.png", ".wri": "icon-text.png", ".htm": "icon-text.png", ".html": "icon-text.png", ".pdf": "icon-pdf.gif", ".ps": "icon-pdf.gif", ".key": "icon-keynote.gif", ".mdb": "icon-mdb.png", ".doc": "icon-word.png", ".ppt": "icon-ppt.gif", ".xls": "icon-excel.png", ".xlsx": "icon-excel.png", }, ) def __str__(self): return self.filename @property def user_initials(self): return self.created.initials @property def icon(self): base, ext = os.path.splitext(self.filename) return self.extensions_to_icons[ext] def user_can_delete(self, user): """ Verify that a user has the appropriate permissions to delete an attachment. """ return self.object_ref.user_can_write(user) def user_can_read(self, user): """ Verify that a user has the appropriate permissions to see (that is, download) an attachment. """ return self.object_ref.user_can_read(user)
class Campaign(edd_models.core.SlugMixin, models.Model): """A grouping of studies, with a broad goal; multiple cycles of DBTL.""" # linking together EDD instances will be easier later if we define UUIDs now uuid = models.UUIDField( editable=False, help_text=_("Unique identifier for this Campaign."), unique=True, verbose_name=_("UUID"), ) name = VarCharField(help_text=_("Name of this Campaign."), verbose_name=_("Name")) description = models.TextField( blank=True, help_text=_("Description of this Campaign."), null=True, verbose_name=_("Description"), ) # create a slug for a more human-readable URL slug = models.SlugField( help_text=_("Slug text used in links to this Campaign."), null=True, unique=True, verbose_name=_("Slug"), ) updates = models.ManyToManyField( edd_models.Update, help_text=_( "List of Update objects logging changes to this Campaign."), related_name="+", verbose_name=_("Updates"), ) # these are used often enough we should save extra queries by including as fields created = models.ForeignKey( edd_models.Update, editable=False, help_text=_("Update used to create this Campaign."), on_delete=models.PROTECT, related_name="+", verbose_name=_("Created"), ) updated = models.ForeignKey( edd_models.Update, editable=False, help_text=_("Update used to last modify this Campaign."), on_delete=models.PROTECT, related_name="+", verbose_name=_("Last Modified"), ) studies = models.ManyToManyField( edd_models.Study, blank=True, help_text=_("Studies that are part of this Campaign."), through="CampaignMembership", verbose_name=_("Studies"), ) @staticmethod def filter_for(user, access=CampaignPermission.CAN_VIEW): """ Similar to main.models.Study.access_filter(); however, this will only build a filter for Campaign objects. These permissions should not be relied upon to cascade to Study objects and children linked by Campaign objects. This call should be used in a queryset .filter() used with a .distinct(); otherwise, if a user has multiple permission paths to a Campaign, multiple results may be returned. """ if isinstance(access, str): access = (access, ) q = Q(everyonepermission__campaign_permission__in=access) if is_real_user(user): q |= Q( userpermission__user=user, userpermission__campaign_permission__in=access, ) | Q( grouppermission__group__user=user, grouppermission__campaign_permission__in=access, ) return q def check_permissions(self, link_type, operation, user): return (is_real_user(user) and user.is_superuser) or any( p.is_allowed(link_type, operation) for p in self.get_permissions(user)) def get_all_permissions(self): return chain( self.userpermission_set.all(), self.grouppermission_set.all(), self.everyonepermission_set.all(), ) def get_permissions(self, user): if is_real_user(user): return chain( self.userpermission_set.filter(user=user), self.grouppermission_set.filter(group__user=user), self.everyonepermission_set.all(), ) return self.everyonepermission_set.all() def user_can_read(self, user): is_super = is_real_user(user) and user.is_superuser has_permission = any(p.is_read() for p in self.get_permissions(user)) return is_super or has_permission def user_can_write(self, user): is_super = is_real_user(user) and user.is_superuser has_permission = any(p.is_write() for p in self.get_permissions(user)) return is_super or has_permission
class Migration(migrations.Migration): dependencies = [ ("main", "0003_remove_carbonsource"), ] operations = [ migrations.CreateModel( name="protocolio", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "name", VarCharField(help_text="Name of this Protocol.", verbose_name="Name"), ), ( "uuid", models.UUIDField( editable=False, help_text="Unique identifier for this Protocol.", unique=True, verbose_name="UUID", ), ), ( "external_url", models.URLField( blank=True, help_text= "The URL in external service (e.g. protocols.io)", null=True, unique=True, ), ), ( "active", models.BooleanField( default=True, help_text= "Flag showing if this Protocol is active and displayed.", verbose_name="Active", ), ), ( "destructive", models.BooleanField( default=False, help_text= "Flag showing if this Protocol consumes a sample.", verbose_name="Destructive", ), ), ( "sbml_category", VarCharField( blank=True, choices=[ ("OD", "Optical Density"), ("HPLC", "HPLC"), ("LCMS", "LCMS"), ("RAMOS", "RAMOS"), ("TPOMICS", "Transcriptomics / Proteomics"), ], default=None, help_text="SBML category for this Protocol.", null=True, verbose_name="SBML Category", ), ), ( "created", models.ForeignKey( editable=False, help_text="Update used to create this Protocol.", on_delete=PROTECT, related_name="protocol_created", to="main.update", verbose_name="Created", ), ), ( "updated", models.ForeignKey( editable=False, help_text="Update used to last modify this Protocol.", on_delete=PROTECT, related_name="protocol_updated", to="main.update", verbose_name="Last Modified", ), ), ], options={"db_table": "main_protocol"}, ), migrations.AddIndex( model_name="protocolio", index=models.Index(fields=["active", "name"], name="main_protoc_active_f664e6_idx"), ), migrations.AddIndex( model_name="protocolio", index=models.Index(fields=["sbml_category"], name="main_protoc_sbml_ca_7d7196_idx"), ), migrations.RunPython(code=copy_to_protocolio, reverse_code=migrations.RunPython.noop), migrations.AddField( model_name="assay", name="protocolio", field=models.ForeignKey( blank=True, null=True, on_delete=PROTECT, to="main.protocolio", ), ), migrations.AddField( model_name="worklisttemplate", name="protocolio", field=models.ForeignKey( blank=True, null=True, on_delete=PROTECT, to="main.protocolio", ), ), migrations.RunPython(code=switch_protocol, reverse_code=migrations.RunPython.noop), migrations.RemoveField(model_name="assay", name="protocol"), migrations.RemoveField(model_name="worklisttemplate", name="protocol"), migrations.DeleteModel(name="protocol"), migrations.RenameField(model_name="assay", old_name="protocolio", new_name="protocol"), migrations.RenameField( model_name="worklisttemplate", old_name="protocolio", new_name="protocol", ), migrations.RenameModel(old_name="protocolio", new_name="protocol"), migrations.AlterField( model_name="assay", name="protocol", field=models.ForeignKey( help_text="The Protocol used to create this Assay.", on_delete=PROTECT, to="main.protocol", verbose_name="Protocol", ), ), migrations.AlterField( model_name="line", name="protocols", field=models.ManyToManyField( help_text="Protocol(s) used to Assay this Line.", through="main.assay", to="main.protocol", verbose_name="Protocol(s)", ), ), migrations.AlterField( model_name="protocol", name="destructive", field=models.BooleanField( default=False, help_text="Flag showing if this protocol consumes a sample.", verbose_name="Destructive", ), ), migrations.AlterField( model_name="study", name="protocols", field=models.ManyToManyField( blank=True, db_table="study_protocol", help_text="Protocols planned for use in this Study.", to="main.protocol", verbose_name="Protocols", ), ), migrations.AlterField( model_name="worklisttemplate", name="protocol", field=models.ForeignKey( help_text="Default protocol for this Template.", on_delete=PROTECT, to="main.protocol", verbose_name="Protocol", ), ), # these fields aren't used and are causing Django to generate infinite migrations migrations.RemoveField( model_name="line", name="protocols", ), migrations.RemoveField( model_name="study", name="protocols", ), ]
class CampaignPermission(BasePermission, models.Model): """Permissions specific to a Campaign.""" ADD = "add" REMOVE = "remove" LEVEL_OVERRIDES = { BasePermission.NONE: (), BasePermission.READ: (BasePermission.NONE, ), BasePermission.WRITE: (BasePermission.NONE, BasePermission.READ), } LINKS = set() class Meta: abstract = True campaign = models.ForeignKey( "Campaign", help_text=_("Campaign this permission applies to."), on_delete=models.CASCADE, verbose_name=_("Campaign"), ) study_permission = VarCharField( choices=BasePermission.TYPE_CHOICE, default=BasePermission.NONE, help_text=_( "Type of permission applied to Studies linked to Campaign."), verbose_name=_("Study Permission"), ) campaign_permission = VarCharField( choices=BasePermission.TYPE_CHOICE, default=BasePermission.NONE, help_text=_("Permission for read/write on the Campaign itself."), verbose_name=_("Campaign Permission"), ) link_permissions = ArrayField( models.TextField(), default=list, help_text=_("Additional permissions applying to this Campaign."), verbose_name=_("Additional Flags"), ) @classmethod def convert_link_type(cls, link_type, operation): return f"{link_type.__module__}.{link_type.__qualname__}:{operation}" @classmethod def register_link(cls, link_type, operation): """ Adds the ability to create permissions for arbitrary types and operations tied to a Campaign. e.g. if code elsewhere adds a Widget type linked to Campaigns, and would like to limit the users that may do the Florf operation on those Widgets: class Widget(models.Model): def user_can_florf(self, user): return any( p.is_allowed(Widget, "florf") for p in self.campaign.get_permissions(user) ) CampaignPermission.register_link(Widget, "florf") """ cls.LINKS.add(cls.convert_link_type(link_type, operation)) @classmethod def unregister_link(cls, link_type, operation): """ Removes a type and operation registration from those available to be managed via CampaignPermission restrictions. """ cls.LINKS.remove(cls.convert_link_type(link_type, operation)) def __getitem__(self, key): # only return boolean for valid keys in self.LINKS if key in self.LINKS: return key in self.link_permissions # templates do getitem lookups before attribute lookups, so fallback to attributes return getattr(self, key) def __setitem__(self, key, value): if key not in self.LINKS: raise ValueError( f"{key} is not registered as a Campaign permission") if value: # avoid adding duplicates if key not in self.link_permissions: self.link_permissions.append(key) else: # remove if present try: self.link_permissions.remove(key) except ValueError: logging.info(f"Removing permission {key} but it was not set") def get_permission_overrides(self): return self.LEVEL_OVERRIDES.get(self.study_permission, []) def get_type_label(self): return dict(self.TYPE_CHOICE).get(self.campaign_permission, "?") def is_allowed(self, link_type, operation): link = self.convert_link_type(link_type, operation) return link in self.link_permissions def is_read(self): """ Test if the permission grants read privileges. :returns: True if permission grants read access """ return self.campaign_permission in self.CAN_VIEW def is_write(self): """ Test if the permission grants write privileges. :returns: True if permission grants write access """ return self.campaign_permission in self.CAN_EDIT def set_allowed(self, link_type, operation, allow=True): """ Change the state of this permission for adding linked objects. :param link_type: the class of object to modify adding link permissions :param allow: boolean state for permission; True allows adding link, False dis-allows adding link. (Default True) """ link = self.convert_link_type(link_type, operation) self[link] = allow
class Protocol(EDDObject): """A defined method of examining a Line.""" class Meta: db_table = "protocol" CATEGORY_NONE = "NA" CATEGORY_OD = "OD" CATEGORY_HPLC = "HPLC" CATEGORY_LCMS = "LCMS" CATEGORY_RAMOS = "RAMOS" CATEGORY_TPOMICS = "TPOMICS" CATEGORY_CHOICE = ( (CATEGORY_NONE, _("None")), (CATEGORY_OD, _("Optical Density")), (CATEGORY_HPLC, _("HPLC")), (CATEGORY_LCMS, _("LCMS")), (CATEGORY_RAMOS, _("RAMOS")), (CATEGORY_TPOMICS, _("Transcriptomics / Proteomics")), ) object_ref = models.OneToOneField(EDDObject, on_delete=models.CASCADE, parent_link=True, related_name="+") owned_by = models.ForeignKey( settings.AUTH_USER_MODEL, help_text=_("Owner / maintainer of this Protocol"), on_delete=models.PROTECT, related_name="protocol_set", verbose_name=_("Owner"), ) variant_of = models.ForeignKey( "self", blank=True, help_text=_( "Link to another original Protocol used as basis for this Protocol." ), null=True, on_delete=models.PROTECT, related_name="derived_set", verbose_name=_("Variant of Protocol"), ) default_units = models.ForeignKey( "MeasurementUnit", blank=True, help_text=_("Default units for values measured with this Protocol."), null=True, on_delete=models.SET_NULL, related_name="protocol_set", verbose_name=_("Default Units"), ) categorization = VarCharField( choices=CATEGORY_CHOICE, default=CATEGORY_NONE, help_text=_("SBML category for this Protocol."), verbose_name=_("SBML Category"), ) def creator(self): return self.created.mod_by def owner(self): return self.owned_by def last_modified(self): return self.updated.mod_time def to_solr_value(self): return f"{self.pk}@{self.name}" def __str__(self): return self.name def save(self, *args, **kwargs): if self.name in ["", None]: raise ValueError("Protocol name required.") p = Protocol.objects.filter(name=self.name) if (self.id is not None and p.count() > 1) or (self.id is None and p.count() > 0): raise ValueError( f"There is already a protocol named '{self.name}'.") return super().save(*args, **kwargs)
class Metabolite(MeasurementType): """ Defines additional metadata on a metabolite measurement type; charge, carbon count, molar mass, molecular formula, SMILES, PubChem CID. """ class Meta: db_table = "metabolite" charge = models.IntegerField(help_text=_("The charge of this molecule."), verbose_name=_("Charge")) carbon_count = models.IntegerField( help_text=_("Count of carbons present in this molecule."), verbose_name=_("Carbon Count"), ) molar_mass = models.DecimalField( decimal_places=5, help_text=_("Molar mass of this molecule."), max_digits=16, verbose_name=_("Molar Mass"), ) molecular_formula = models.TextField( help_text=_("Formula string defining this molecule."), verbose_name=_("Formula")) smiles = VarCharField( blank=True, help_text=_("SMILES string defining molecular structure."), null=True, verbose_name=_("SMILES"), ) pubchem_cid = models.IntegerField( blank=True, help_text=_("Unique PubChem identifier"), null=True, unique=True, verbose_name=_("PubChem CID"), ) id_map = ArrayField( VarCharField(), default=list, help_text=_( "List of identifiers mapping to external chemical datasets."), verbose_name=_("External IDs"), ) tags = ArrayField( VarCharField(), default=list, help_text=_("List of tags for classifying this molecule."), verbose_name=_("Tags"), ) carbon_pattern = re.compile(r"C(?![a-z])(\d*)") pubchem_pattern = re.compile(r"(?i)cid:\s*(\d+)(?::(.*))?") def __str__(self): return self.type_name def is_metabolite(self): return True def to_json(self, depth=0): """Export a serializable dictionary.""" return dict( super().to_json(), **{ "formula": self.molecular_formula, "molar": float(self.molar_mass), "carbons": self.carbon_count, "pubchem": self.pubchem_cid, "smiles": self.smiles, }, ) def to_solr_json(self): """Convert the MeasurementType model to a dict structure formatted for Solr JSON.""" return dict( super().to_solr_json(), **{ "m_charge": self.charge, "m_carbons": self.carbon_count, "m_mass": self.molar_mass, "m_formula": self.molecular_formula, "m_tags": list(self.tags), }, ) def save(self, *args, **kwargs): if self.carbon_count is None: self.carbon_count = self.extract_carbon_count() # force METABOLITE group self.type_group = MeasurementType.Group.METABOLITE super().save(*args, **kwargs) def extract_carbon_count(self): count = 0 for match in self.carbon_pattern.finditer(self.molecular_formula): c = match.group(1) count = count + (int(c) if c else 1) return count def _load_pubchem(self, pubchem_cid): base_url = ( f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{pubchem_cid}" ) try: self.pubchem_cid = pubchem_cid if self._load_pubchem_name(base_url) and self._load_pubchem_props( base_url): self.type_source = Datasource.objects.create(name="PubChem", url=base_url) self.carbon_count = self.extract_carbon_count() self.provisional = False self.save() return True logger.warn(f"Skipped saving PubChem info for {pubchem_cid}") except Exception: logger.exception( f"Failed processing PubChem info for {pubchem_cid}") return False def _load_pubchem_name(self, base_url): # the default properties listing does not give common names, synonyms list does try: response = requests.get(f"{base_url}/synonyms/JSON") # payload is nested in this weird envelope names = response.json( )["InformationList"]["Information"][0]["Synonym"] # set the first synonym self.type_name = next(iter(names)) return True except Exception: logger.exception( f"Failed loading names from PubChem for {self.pubchem_cid}") return False def _load_pubchem_props(self, base_url): # can list out only specific properties needed in URL props = "MolecularFormula,MolecularWeight,Charge,CanonicalSMILES" try: response = requests.get(f"{base_url}/property/{props}/JSON") # payload is nested in this weird envelope table = response.json()["PropertyTable"]["Properties"][0] # set the properties found self.charge = table.get("Charge", 0) self.molecular_formula = table.get("MolecularFormula", "") self.molar_mass = table.get("MolecularWeight", 1) self.smiles = table.get("CanonicalSMILES", "") return True except Exception: logger.exception( f"Failed loading properties from Pubchem for {self.pubchem_cid}" ) return False @classmethod def load_or_create(cls, pubchem_cid): match = cls.pubchem_pattern.match(pubchem_cid) if match: cid = match.group(1) label = match.group(2) # try to find existing Metabolite record metabolite, created = cls.objects.get_or_create( pubchem_cid=cid, defaults={ "carbon_count": 0, "charge": 0, "molar_mass": 1, "molecular_formula": "", "provisional": True, "type_group": MeasurementType.Group.METABOLITE, "type_name": label or "Unknown Metabolite", }, ) if created: transaction.on_commit( lambda: metabolite_load_pubchem.delay(metabolite.pk)) return metabolite raise ValidationError( _u('Metabolite lookup failed: {pubchem} must match pattern "cid:0000"' ).format(pubchem=pubchem_cid))
class Migration(migrations.Migration): replaces = [ ("profile", "0001_initial"), ("profile", "0002_auto_20150729_1523"), ("profile", "0003_usertask"), ("profile", "0004_userprofile_preferences"), ("profile", "0005_remove_hstore"), ("profile", "0006_remove_usertask"), ("profile", "0007_use_varchar"), ("profile", "0008_add_approval_flag"), ("profile", "0009_add_institution_order"), ] initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("auth", "0011_update_proxy_permissions"), ] operations = [ migrations.CreateModel( name="User", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("password", models.CharField(max_length=128, verbose_name="password")), ( "last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login"), ), ( "is_superuser", models.BooleanField( default=False, help_text= "Designates that this user has all permissions " "without explicitly assigning them.", verbose_name="superuser status", ), ), ( "username", models.CharField( error_messages={ "unique": "A user with that username already exists." }, help_text="Required. 150 characters or fewer. " "Letters, digits and @/./+/-/_ only.", max_length=150, unique=True, validators=[UnicodeUsernameValidator()], verbose_name="username", ), ), ( "first_name", models.CharField(blank=True, max_length=150, verbose_name="first name"), ), ( "last_name", models.CharField(blank=True, max_length=150, verbose_name="last name"), ), ( "email", models.EmailField(blank=True, max_length=254, verbose_name="email address"), ), ( "is_staff", models.BooleanField( default=False, help_text= "Designates whether the user can log into this admin site.", verbose_name="staff status", ), ), ( "is_active", models.BooleanField( default=True, help_text= "Designates whether this user should be treated as active. " "Unselect this instead of deleting accounts.", verbose_name="active", ), ), ( "date_joined", models.DateTimeField(default=timezone.now, verbose_name="date joined"), ), ( "groups", models.ManyToManyField( blank=True, help_text="The groups this user belongs to. " "A user will get all permissions granted to each of their groups.", related_name="user_set", related_query_name="user", to="auth.Group", verbose_name="groups", ), ), ( "user_permissions", models.ManyToManyField( blank=True, help_text="Specific permissions for this user.", related_name="user_set", related_query_name="user", to="auth.Permission", verbose_name="user permissions", ), ), ], options={"db_table": "auth_user"}, managers=[("profiles", ProfileUserManager()), ("objects", UserManager())], ), migrations.CreateModel( name="Institution", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("institution_name", VarCharField()), ("description", models.TextField(blank=True, null=True)), ], options={"db_table": "profile_institution"}, ), migrations.CreateModel( name="InstitutionID", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("identifier", VarCharField(blank=True, null=True)), ( "sort_key", models.PositiveIntegerField( help_text= "Relative order this Institution is displayed in a UserProfile.", verbose_name="Display order", ), ), ( "institution", models.ForeignKey( on_delete=models.deletion.CASCADE, to="profile.Institution", ), ), ], options={"db_table": "profile_institution_user"}, ), migrations.CreateModel( name="UserProfile", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("initials", VarCharField(blank=True, null=True)), ("description", models.TextField(blank=True, null=True)), ( "preferences", models.JSONField(blank=True, default=dict), ), ( "approved", models.BooleanField( default=False, help_text= "Flag showing if this account has been approved for login.", verbose_name="Approved", ), ), ( "institutions", models.ManyToManyField(through="profile.InstitutionID", to="profile.Institution"), ), ( "user", models.OneToOneField( on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, ), ), ], options={"db_table": "profile_user"}, ), migrations.AddField( model_name="institutionid", name="profile", field=models.ForeignKey(on_delete=models.deletion.CASCADE, to="profile.UserProfile"), ), migrations.AddConstraint( model_name="institutionid", constraint=models.UniqueConstraint( fields=("profile", "sort_key"), name="profile_institution_ordering_idx"), ), ]
class StudyLog(models.Model): """Recorded entry for Study metrics captured by EDD.""" class Event(models.TextChoices): CREATED = "STUDY_CREATED", _("Study Created") DESCRIBED = "STUDY_DESCRIBED", _("Study Described") EXPORTED = "STUDY_EXPORTED", _("Study Exported") IMPORTED = "STUDY_IMPORTED", _("Study Imported") PERMISSION = "STUDY_PERMISSION", _("Study Permission Changed") VIEWED = "STUDY_VIEWED", _("Study Viewed") WORKLIST = "STUDY_WORKLIST", _("Study Worklist") class Meta: verbose_name = _("Study Log") verbose_name_plural = _("Study Logs") # should never need to reference this, but need a primary key _id = models.UUIDField( default=uuid.uuid4, editable=False, name="id", primary_key=True, unique=True, ) # store extra values that only exist on certain events here detail = models.JSONField( blank=True, decoder=JSONDecoder, editable=False, encoder=JSONEncoder, help_text=_( "JSON structure with extra details specific to the event."), default=dict, verbose_name=_("Details"), ) event = VarCharField( blank=False, choices=Event.choices, editable=False, help_text=_("Type of logged metric event."), verbose_name=_("Event"), ) study = models.ForeignKey( edd_models.Study, blank=True, editable=False, help_text=_("The Study associated with the logged metric event."), on_delete=models.SET_NULL, null=True, related_name="metric_log", verbose_name=_("Study"), ) timestamp = models.DateTimeField( auto_now_add=True, editable=False, help_text=_("Timestamp of the logged metric event."), verbose_name=_("Timestamp"), ) user = models.ForeignKey( settings.AUTH_USER_MODEL, editable=False, help_text=_("The user triggering the logged metric event."), null=True, on_delete=models.SET_NULL, verbose_name=_("User"), ) @classmethod def lookup_study(cls, study_id): try: return edd_models.Study.objects.get(id=study_id) except edd_models.Study.DoesNotExist: return None