Test htsworkflow under several different django & python versions
[htsworkflow.git] / samples / models.py
1 from __future__ import unicode_literals
2
3 import logging
4 from django.db import models
5 from django.contrib.auth.models import User
6 from django.core import urlresolvers
7 from django.db.models.signals import post_save
8 from django.db import connection
9 from django.core.validators import RegexValidator
10 import six
11
12 logger = logging.getLogger(__name__)
13
14
15 class AccessionAgency(models.Model):
16     """An organization one submits data to
17     """
18     name = models.CharField(max_length=255)
19     homepage = models.URLField(blank=True)
20     library_template = models.URLField(blank=True)
21
22     class Meta:
23         verbose_name_plural = 'Accession Agencies'
24
25     def __str__(self):
26         return self.name
27
28
29 class Accession(models.Model):
30     """Track accession IDs assigned to our objects.
31     """
32     accession = models.CharField(
33         max_length=255,
34         db_index=True,
35         validators=[RegexValidator(
36             "^[-A-Za-z0-9:.]*$",
37             message="Please only use letters, digits, and :.-")]
38     )
39     url = models.URLField(blank=True, null=True)
40     agency = models.ForeignKey(AccessionAgency)
41     created = models.DateTimeField()
42
43     class Meta:
44         abstract = True
45
46     def update_url(self, template):
47         if template and not self.url:
48             self.url = template.format(self.accession)
49
50     def __str__(self):
51         return str(self.agency) + ":" + self.accession
52
53
54 class LibraryAccession(Accession):
55     library = models.ForeignKey('Library')
56
57     def save(self, *args, **kwargs):
58         self.update_url(self.agency.library_template)
59         super(LibraryAccession, self).save(*args, **kwargs)
60
61
62 class Antibody(models.Model):
63     antigene = models.CharField(max_length=500, db_index=True)
64     # New field Aug/20/08
65     # SQL to add column:
66     # alter table fctracker_antibody add column "nickname" varchar(20) NULL;
67     nickname = models.CharField(
68         max_length=20,
69         blank=True,
70         null=True,
71         db_index=True
72     )
73     catalog = models.CharField(max_length=50, blank=True, null=True)
74     antibodies = models.CharField(max_length=500, db_index=True)
75     source = models.CharField(max_length=500,
76                               blank=True, null=True, db_index=True)
77     biology = models.TextField(blank=True, null=True)
78     notes = models.TextField(blank=True, null=True)
79
80     def __str__(self):
81         return '%s - %s' % (self.antigene, self.antibodies)
82
83     class Meta:
84         verbose_name_plural = "antibodies"
85         ordering = ["antigene"]
86
87
88 class Cellline(models.Model):
89     cellline_name = models.CharField(max_length=100,
90                                      unique=True, db_index=True)
91     nickname = models.CharField(
92         max_length=20,
93         blank=True,
94         null=True,
95         db_index=True)
96
97     notes = models.TextField(blank=True)
98
99     def __str__(self):
100         return str(self.cellline_name)
101
102     class Meta:
103         ordering = ["cellline_name"]
104
105
106 class Condition(models.Model):
107     condition_name = models.CharField(
108         max_length=2000, unique=True, db_index=True)
109     nickname = models.CharField(
110         max_length=20,
111         blank=True,
112         null=True,
113         db_index=True,
114         verbose_name='Short Name')
115     notes = models.TextField(blank=True)
116
117     def __str__(self):
118         return str(self.condition_name)
119
120     class Meta:
121         ordering = ["condition_name"]
122
123
124 class ExperimentType(models.Model):
125     name = models.CharField(max_length=50, unique=True)
126
127     def __str__(self):
128         return str(self.name)
129
130
131 class Tag(models.Model):
132     tag_name = models.CharField(max_length=100,
133                                 db_index=True, blank=False, null=False)
134     TAG_CONTEXT = (
135         # ('Antibody','Antibody'),
136         # ('Cellline', 'Cellline'),
137         # ('Condition', 'Condition'),
138         ('Library', 'Library'),
139         ('ANY', 'ANY'),
140     )
141     context = models.CharField(
142         max_length=50,
143         choices=TAG_CONTEXT, default='Library')
144
145     def __str__(self):
146         return '%s' % (self.tag_name,)
147
148     class Meta:
149         ordering = ["context", "tag_name"]
150
151
152 class Species(models.Model):
153     scientific_name = models.CharField(
154         max_length=256,
155         unique=False,
156         db_index=True
157     )
158     common_name = models.CharField(max_length=256, blank=True)
159
160     def __str__(self):
161         return '%s (%s)' % (self.scientific_name, self.common_name)
162
163     class Meta:
164         verbose_name_plural = "species"
165         ordering = ["scientific_name"]
166
167     def get_absolute_url(self):
168         return urlresolvers.reverse('species', kwargs={'species_id': str(self.id)})
169
170
171 class Affiliation(models.Model):
172     name = models.CharField(max_length=256, db_index=True, verbose_name='Name')
173     contact = models.CharField(max_length=256,
174                                null=True, blank=True, verbose_name='Lab Name')
175     email = models.EmailField(null=True, blank=True)
176     users = models.ManyToManyField('HTSUser', blank=True)
177     users.admin_order_field = "username"
178
179     def __str__(self):
180         name = str(self.name)
181         if self.contact is not None and len(self.contact) > 0:
182             name += ' ('+self.contact+')'
183         return name
184
185     def Users(self):
186         users = self.users.all().order_by('username')
187         return ", ".join([str(a) for a in users])
188
189     class Meta:
190         ordering = ["name", "contact"]
191         unique_together = (("name", "contact"),)
192
193
194 class LibraryType(models.Model):
195     name = models.CharField(max_length=255, unique=True,
196                             verbose_name="Adapter Type")
197     is_paired_end = models.BooleanField(
198         default=True,
199         help_text="can you do a paired end run with this adapter")
200     can_multiplex = models.BooleanField(
201         default=True,
202         help_text="Does this adapter provide multiplexing?")
203
204     def __str__(self):
205         return str(self.name)
206
207     class Meta:
208         ordering = ["-id"]
209
210
211 class MultiplexIndex(models.Model):
212     """Map adapter types to the multiplex sequence"""
213     adapter_type = models.ForeignKey(LibraryType)
214     multiplex_id = models.CharField(max_length=6, null=False)
215     sequence = models.CharField(max_length=12, blank=True, null=True)
216
217     class Meta:
218         verbose_name_plural = "multiplex indicies"
219         unique_together = ('adapter_type', 'multiplex_id')
220
221
222 class Library(models.Model):
223     id = models.CharField(max_length=10, primary_key=True)
224     library_name = models.CharField(max_length=100, unique=True)
225     library_species = models.ForeignKey(Species)
226     hidden = models.BooleanField(default=False)
227     account_number = models.CharField(max_length=100, null=True, blank=True)
228     cell_line = models.ForeignKey(Cellline, blank=True, null=True,
229                                   verbose_name="Background")
230     condition = models.ForeignKey(Condition, blank=True, null=True)
231     antibody = models.ForeignKey(Antibody, blank=True, null=True)
232     affiliations = models.ManyToManyField(
233         Affiliation, related_name='library_affiliations')
234     tags = models.ManyToManyField(Tag, related_name='library_tags',
235                                   blank=True)
236     REPLICATE_NUM = [(x, x) for x in range(1, 7)]
237     replicate = models.PositiveSmallIntegerField(choices=REPLICATE_NUM,
238                                                  blank=True, null=True)
239     experiment_type = models.ForeignKey(ExperimentType)
240     library_type = models.ForeignKey(LibraryType, blank=True, null=True,
241                                      verbose_name="Adapter Type")
242     multiplex_id = models.CharField(max_length=255,
243                                     blank=True, null=True,
244                                     verbose_name="Index ID")
245     creation_date = models.DateField(blank=True, null=True)
246     made_for = models.CharField(max_length=50, blank=True,
247                                 verbose_name='ChIP/DNA/RNA Made By')
248     made_by = models.CharField(max_length=50, blank=True, default="Lorian")
249
250     PROTOCOL_END_POINTS = (
251         ('?', 'Unknown'),
252         ('Sample', 'Raw sample'),
253         ('Progress', 'In progress'),
254         ('1A', 'Ligation, then gel'),
255         ('PCR', 'Ligation, then PCR'),
256         ('1Ab', 'Ligation, PCR, then gel'),
257         ('1Ac', 'Ligation, gel, then 12x PCR'),
258         ('1Aa', 'Ligation, gel, then 18x PCR'),
259         ('2A', 'Ligation, PCR, gel, PCR'),
260         ('Done', 'Completed'),
261     )
262     PROTOCOL_END_POINTS_DICT = dict(PROTOCOL_END_POINTS)
263     stopping_point = models.CharField(max_length=25,
264                                       choices=PROTOCOL_END_POINTS,
265                                       default='Done')
266
267     amplified_from_sample = models.ForeignKey(
268         'self',
269         related_name='amplified_into_sample',
270         blank=True, null=True)
271
272     undiluted_concentration = models.DecimalField(
273         "Concentration",
274         max_digits=5, decimal_places=2, blank=True, null=True,
275         help_text="Undiluted concentration (ng/\u00b5l)")
276     # note \u00b5 is the micro symbol in unicode
277     successful_pM = models.DecimalField(
278         max_digits=9, decimal_places=1, blank=True, null=True)
279     ten_nM_dilution = models.BooleanField(default=False)
280     gel_cut_size = models.IntegerField(default=225, blank=True, null=True)
281     insert_size = models.IntegerField(blank=True, null=True)
282     notes = models.TextField(blank=True)
283
284     bioanalyzer_summary = models.TextField(blank=True, default="")
285     bioanalyzer_concentration = models.DecimalField(
286         max_digits=5, decimal_places=2, blank=True, null=True,
287         help_text="(ng/\u00b5l)")
288     bioanalyzer_image_url = models.URLField(blank=True, default="")
289
290     def __str__(self):
291         return '#%s: %s' % (self.id, self.library_name)
292
293     class Meta:
294         verbose_name_plural = "libraries"
295         # ordering = ["-creation_date"]
296         ordering = ["-id"]
297
298     def antibody_name(self):
299         str = '<a target=_self href="/admin/samples/antibody/' + \
300               self.antibody.id.__str__() + \
301               '/" title="' + self.antibody.__str__() + '">' + \
302               self.antibody.label+'</a>'
303         return str
304     antibody_name.allow_tags = True
305
306     def organism(self):
307         return self.library_species.common_name
308
309     def index_sequences(self):
310         """Return a dictionary of multiplex index id to sequence
311         Return None if the library can't multiplex,
312         """
313         if self.library_type is None:
314             return None
315         if not self.library_type.can_multiplex:
316             return None
317         if self.multiplex_id is None or len(self.multiplex_id) == 0:
318             return 'Err: id empty'
319         sequences = {}
320         multiplex_expressions = self.multiplex_id.split(',')
321         for multiplex_term in multiplex_expressions:
322             pairs = multiplex_term.split('-')
323             if len(pairs) == 1:
324                 key = pairs[0]
325                 seq = self._lookup_index(pairs[0])
326             elif len(pairs) == 2:
327                 key = pairs[0] + '-' + pairs[1]
328                 seq0 = self._lookup_index(pairs[0])
329                 seq1 = self._lookup_index(pairs[1])
330                 if seq0 is None or seq1 is None:
331                     seq = None
332                 else:
333                     seq = seq0 + '-' + seq1
334             else:
335                 raise RuntimeError("Too many - seperated sequences")
336             if seq is None:
337                 seq = 'Err: index not found'
338             sequences[key] = seq
339         return sequences
340
341     def _lookup_index(self, multiplex_id):
342         try:
343             multiplex = MultiplexIndex.objects.get(
344                 adapter_type=self.library_type.id,
345                 multiplex_id=multiplex_id)
346             return multiplex.sequence
347         except MultiplexIndex.DoesNotExist as e:
348             return None
349
350     def index_sequence_text(self, seperator=' '):
351         """Return formatted multiplex index sequences"""
352         sequences = self.index_sequences()
353         if sequences is None:
354             return ""
355         if isinstance(sequences, six.string_types):
356             return sequences
357         multiplex_ids = sorted(sequences)
358         return seperator.join(
359             ("%s:%s" % (i, sequences[i]) for i in multiplex_ids))
360     index_sequence_text.short_description = "Index"
361
362     def affiliation(self):
363         affs = self.affiliations.all().order_by('name')
364         tstr = ''
365         ar = []
366         for t in affs:
367             ar.append(t.__str__())
368         return '%s' % (", ".join(ar))
369
370     def is_archived(self):
371         """returns True if archived else False
372         """
373         if self.longtermstorage_set.count() > 0:
374             return True
375         else:
376             return False
377
378     def lanes_sequenced(self):
379         """Count how many lanes of each type were run.
380         """
381         single = 0
382         paired = 1
383         short_read = 0
384         medium_read = 1
385         long_read = 2
386         counts = [[0, 0, 0], [0, 0, 0]]
387
388         for lane in self.lane_set.all():
389             if lane.flowcell.paired_end:
390                 lane_type = paired
391             else:
392                 lane_type = single
393
394             if lane.flowcell.read_length < 40:
395                 read_type = short_read
396             elif lane.flowcell.read_length < 100:
397                 read_type = medium_read
398             else:
399                 read_type = long_read
400             counts[lane_type][read_type] += 1
401
402         return counts
403
404     def stopping_point_name(self):
405         end_points = Library.PROTOCOL_END_POINTS_DICT
406         name = end_points.get(self.stopping_point, None)
407         if name is None:
408             name = "Lookup Error"
409             logger.error("protocol stopping point in database"
410                          "didn't match names in library model")
411         return name
412
413     def libtags(self):
414         affs = self.tags.all().order_by('tag_name')
415         ar = []
416         for t in affs:
417             ar.append(t.__str__())
418         return '%s' % (", ".join(ar))
419
420     def DataRun(self):
421         str = '<a target=_self href="/admin/experiments/datarun/?q=' + \
422               self.id + \
423               '" title="Check All Data Runs for This Specific Library ..."' \
424               '">Data Run</a>'
425         return str
426     DataRun.allow_tags = True
427
428     def aligned_m_reads(self):
429         return getLibReads(self.id)
430
431     def aligned_reads(self):
432         res = getLibReads(self.id)
433
434         # Check data sanity
435         if res[2] != "OK":
436             return '<div style="border:solid red 2px">'+res[2]+'</div>'
437
438         rc = "%1.2f" % (res[1]/1000000.0)
439         # Color Scheme: green is more than 10M, blue is more than 5M, orange is more than 3M and red is less. For RNAseq, all those thresholds should be doubled
440         if res[0] > 0:
441             bgcolor = '#ff3300'  # Red
442             rc_thr = [10000000, 5000000, 3000000]
443             if self.experiment_type == 'RNA-seq':
444                 rc_thr = [20000000, 10000000, 6000000]
445
446             if res[1] > rc_thr[0]:
447                 bgcolor = '#66ff66'  # Green
448             else:
449                 if res[1] > rc_thr[1]:
450                     bgcolor = '#00ccff'  # Blue
451                 else:
452                     if res[1] > rc_thr[2]:
453                         bgcolor = '#ffcc33'  # Orange
454             tstr = '<div style="background-color:'+bgcolor+';color:black">'
455             tstr += res[0].__str__()+' Lanes, '+rc+' M Reads'
456             tstr += '</div>'
457         else:
458             tstr = 'not processed yet'
459         return tstr
460     aligned_reads.allow_tags = True
461
462     def public(self):
463         summary_url = self.get_absolute_url()
464         return '<a href="%s">S</a>' % (summary_url,)
465     public.allow_tags = True
466
467     def get_absolute_url(self):
468         return urlresolvers.reverse('library_to_flowcells',
469                                     kwargs={'lib_id': str(self.id)})
470
471     def get_admin_url(self):
472         return urlresolvers.reverse('admin:samples_library_change',
473                                     args=(self.id,))
474
475
476 class HTSUser(User):
477     """
478     Provide some site-specific customization for the django user class
479     """
480     # objects = UserManager()
481
482     class Meta:
483         proxy = True
484         ordering = ['first_name', 'last_name', 'username']
485
486     def admin_url(self):
487         return '/admin/%s/%s/%d' % (self._meta.app_label,
488                                     self._meta.module_name, self.id)
489
490     def __str__(self):
491         # return str(self.username) + " (" + str(self.get_full_name()) + u")"
492         return str(self.get_full_name()) + ' (' + str(self.username) + ')'
493
494
495 def HTSUserInsertID(sender, instance, **kwargs):
496     """
497     Force addition of HTSUsers when someone just modifies the auth_user object
498     """
499     u = HTSUser.objects.filter(pk=instance.id)
500     if len(u) == 0:
501         cursor = connection.cursor()
502         cursor.execute(
503             'INSERT INTO samples_htsuser (user_ptr_id) VALUES (%s);' %
504             (instance.id,))
505         cursor.close()
506
507 post_save.connect(HTSUserInsertID, sender=User)