Add support for tracking the multiplex index sequence.
[htsworkflow.git] / htsworkflow / frontend / samples / models.py
1 import logging
2 import urlparse
3 from django.db import models
4 from django.contrib.auth.models import User, UserManager
5 from django.core import urlresolvers
6 from django.db.models.signals import pre_save, post_save
7 from django.db import connection
8 from htsworkflow.frontend.reports.libinfopar import *
9
10 logger = logging.getLogger(__name__)
11
12 class Antibody(models.Model):
13     antigene = models.CharField(max_length=500, db_index=True)
14     # New field Aug/20/08
15     # SQL to add column:
16     # alter table fctracker_antibody add column "nickname" varchar(20) NULL;
17     nickname = models.CharField(
18         max_length=20,
19         blank=True,
20         null=True,
21         db_index=True
22     )
23     catalog = models.CharField(max_length=50, blank=True, null=True)
24     antibodies = models.CharField(max_length=500, db_index=True)
25     source = models.CharField(max_length=500, blank=True, null=True, db_index=True)
26     biology = models.TextField(blank=True, null=True)
27     notes = models.TextField(blank=True, null=True)
28     def __unicode__(self):
29         return u'%s - %s' % (self.antigene, self.antibodies)
30     class Meta:
31         verbose_name_plural = "antibodies"
32         ordering = ["antigene"]
33
34 class Cellline(models.Model):
35     cellline_name = models.CharField(max_length=100, unique=True, db_index=True)
36     nickname = models.CharField(max_length=20,
37         blank=True,
38         null=True,
39         db_index=True)
40
41     notes = models.TextField(blank=True)
42     def __unicode__(self):
43         return unicode(self.cellline_name)
44
45     class Meta:
46         ordering = ["cellline_name"]
47
48 class Condition(models.Model):
49     condition_name = models.CharField(
50         max_length=2000, unique=True, db_index=True)
51     nickname = models.CharField(max_length=20,
52         blank=True,
53         null=True,
54         db_index=True,
55         verbose_name = 'Short Name')
56     notes = models.TextField(blank=True)
57
58     def __unicode__(self):
59         return unicode(self.condition_name)
60
61     class Meta:
62         ordering = ["condition_name"]
63
64
65 class ExperimentType(models.Model):
66   name = models.CharField(max_length=50, unique=True)
67
68   def __unicode__(self):
69     return unicode(self.name)
70
71 class Tag(models.Model):
72   tag_name = models.CharField(max_length=100, db_index=True,blank=False,null=False)
73   TAG_CONTEXT = (
74       #('Antibody','Antibody'),
75       #('Cellline', 'Cellline'),
76       #('Condition', 'Condition'),
77       ('Library', 'Library'),
78       ('ANY','ANY'),
79   )
80   context = models.CharField(max_length=50,
81       choices=TAG_CONTEXT, default='Library')
82
83   def __unicode__(self):
84     return u'%s' % (self.tag_name)
85
86   class Meta:
87     ordering = ["context","tag_name"]
88
89 class Species(models.Model):
90   scientific_name = models.CharField(max_length=256,
91       unique=False,
92       db_index=True
93   )
94   common_name = models.CharField(max_length=256, blank=True)
95   #use_genome_build = models.CharField(max_length=100, blank=False, null=False)
96
97   def __unicode__(self):
98     return u'%s (%s)' % (self.scientific_name, self.common_name)
99
100   class Meta:
101     verbose_name_plural = "species"
102     ordering = ["scientific_name"]
103
104   @models.permalink
105   def get_absolute_url(self):
106     return ('htsworkflow.frontend.samples.views.species', [str(self.id)])
107
108 class Affiliation(models.Model):
109   name = models.CharField(max_length=256, db_index=True, verbose_name='Name')
110   contact = models.CharField(max_length=256, null=True, blank=True,verbose_name='Lab Name')
111   email = models.EmailField(null=True,blank=True)
112   users = models.ManyToManyField('HTSUser', null=True, blank=True)
113   users.admin_order_field = "username"
114
115   def __unicode__(self):
116     str = unicode(self.name)
117     if self.contact is not None and len(self.contact) > 0:
118       str += u' ('+self.contact+u')'
119     return str
120
121   def Users(self):
122       users = self.users.all().order_by('username')
123       return ", ".join([unicode(a) for a in users ])
124
125   class Meta:
126     ordering = ["name","contact"]
127     unique_together = (("name", "contact"),)
128
129 class LibraryType(models.Model):
130   name = models.CharField(max_length=255, unique=True,
131                           name="Adapter Type")
132   is_paired_end = models.BooleanField(default=True,
133                     help_text="can you do a paired end run with this adapter")
134   can_multiplex = models.BooleanField(default=True,
135                     help_text="Does this adapter provide multiplexing?")
136
137   def __unicode__(self):
138       return unicode(self.name)
139
140   class Meta:
141       ordering = ["-id"]
142
143
144 class MultiplexIndex(models.Model):
145     """Map adapter types to the multiplex sequence"""
146     adapter_type = models.ForeignKey(LibraryType)
147     multiplex_id = models.CharField(max_length=3, null=False)
148     sequence = models.CharField(max_length=12, blank=True, null=True)
149
150     class Meta:
151         unique_together = ('adapter_type', 'multiplex_id')
152
153 class Library(models.Model):
154   id = models.CharField(max_length=10, primary_key=True)
155   library_name = models.CharField(max_length=100, unique=True)
156   library_species = models.ForeignKey(Species)
157   hidden = models.BooleanField()
158   account_number = models.CharField(max_length=100, null=True, blank=True)
159   cell_line = models.ForeignKey(Cellline, blank=True, null=True,
160                                 verbose_name="Background")
161   condition = models.ForeignKey(Condition, blank=True, null=True)
162   antibody = models.ForeignKey(Antibody,blank=True,null=True)
163   affiliations = models.ManyToManyField(
164       Affiliation,related_name='library_affiliations',null=True)
165   tags = models.ManyToManyField(Tag,related_name='library_tags',
166                                 blank=True,null=True)
167   REPLICATE_NUM = ((1,1),(2,2),(3,3),(4,4))
168   replicate =  models.PositiveSmallIntegerField(choices=REPLICATE_NUM,
169                                                 blank=True,null=True)
170   experiment_type = models.ForeignKey(ExperimentType)
171   library_type = models.ForeignKey(LibraryType, blank=True, null=True,
172                                    verbose_name="Adapter Type")
173   multiplex_id = models.CharField(max_length=128,
174                                   blank=True, null=True,
175                                   verbose_name="Index ID")
176   creation_date = models.DateField(blank=True, null=True)
177   made_for = models.CharField(max_length=50, blank=True,
178                               verbose_name='ChIP/DNA/RNA Made By')
179   made_by = models.CharField(max_length=50, blank=True, default="Lorian")
180
181   PROTOCOL_END_POINTS = (
182       ('?', 'Unknown'),
183       ('Sample', 'Raw sample'),
184       ('Progress', 'In progress'),
185       ('1A', 'Ligation, then gel'),
186       ('PCR', 'Ligation, then PCR'),
187       ('1Ab', 'Ligation, PCR, then gel'),
188       ('1Ac', 'Ligation, gel, then 12x PCR'),
189       ('1Aa', 'Ligation, gel, then 18x PCR'),
190       ('2A', 'Ligation, PCR, gel, PCR'),
191       ('Done', 'Completed'),
192     )
193   PROTOCOL_END_POINTS_DICT = dict(PROTOCOL_END_POINTS)
194   stopping_point = models.CharField(max_length=25,
195                                     choices=PROTOCOL_END_POINTS,
196                                     default='Done')
197
198   amplified_from_sample = models.ForeignKey('self',
199                             related_name='amplified_into_sample',
200                             blank=True, null=True)
201
202   undiluted_concentration = models.DecimalField("Concentration",
203       max_digits=5, decimal_places=2, blank=True, null=True,
204       help_text=u"Undiluted concentration (ng/\u00b5l)")
205       # note \u00b5 is the micro symbol in unicode
206   successful_pM = models.DecimalField(max_digits=9,
207                                       decimal_places=1, blank=True, null=True)
208   ten_nM_dilution = models.BooleanField()
209   gel_cut_size = models.IntegerField(default=225, blank=True, null=True)
210   insert_size = models.IntegerField(blank=True, null=True)
211   notes = models.TextField(blank=True)
212
213   bioanalyzer_summary = models.TextField(blank=True,default="")
214   bioanalyzer_concentration = models.DecimalField(max_digits=5,
215                                 decimal_places=2, blank=True, null=True,
216                                 help_text=u"(ng/\u00b5l)")
217   bioanalyzer_image_url = models.URLField(blank=True,default="")
218
219   def __unicode__(self):
220     return u'#%s: %s' % (self.id, self.library_name)
221
222   class Meta:
223       verbose_name_plural = "libraries"
224       #ordering = ["-creation_date"]
225       ordering = ["-id"]
226
227   def antibody_name(self):
228     str ='<a target=_self href="/admin/samples/antibody/'+self.antibody.id.__str__()+'/" title="'+self.antibody.__str__()+'">'+self.antibody.label+'</a>'
229     return str
230   antibody_name.allow_tags = True
231
232   def organism(self):
233     return self.library_species.common_name
234
235   def index_sequences(self):
236       """Return a dictionary of multiplex index id to sequence
237       Return None if the library can't multiplex,
238
239       """
240       if self.library_type is None:
241           return None
242       if not self.library_type.can_multiplex:
243           return None
244       if self.multiplex_id is None or len(self.multiplex_id) == 0:
245           return 'Err: id empty'
246       sequences = {}
247       multiplex_ids = self.multiplex_id.split(',')
248       for multiplex_id in multiplex_ids:
249           try:
250               multiplex = MultiplexIndex.objects.get(
251                   adapter_type = self.library_type.id,
252                   multiplex_id = multiplex_id)
253               sequences[multiplex_id] = multiplex.sequence
254           except MultiplexIndex.DoesNotExist, e:
255               sequences[multiplex_id] = 'Err: index not found'
256       return sequences
257
258   def index_sequence_text(self, seperator=' '):
259       """Return formatted multiplex index sequences"""
260       sequences = self.index_sequences()
261       multiplex_ids = sequences.keys()
262       multiplex_ids.sort()
263       return seperator.join(( "%s:%s" %(i,sequences[i]) for i in multiplex_ids))
264   index_sequence_text.short_description = "Index"
265
266
267   def affiliation(self):
268     affs = self.affiliations.all().order_by('name')
269     tstr = ''
270     ar = []
271     for t in affs:
272         ar.append(t.__unicode__())
273     return '%s' % (", ".join(ar))
274
275   def is_archived(self):
276     """
277     returns True if archived else False
278     """
279     if self.longtermstorage_set.count() > 0:
280         return True
281     else:
282         return False
283
284   def stopping_point_name(self):
285       end_points = Library.PROTOCOL_END_POINTS_DICT
286       name = end_points.get(self.stopping_point, None)
287       if name is None:
288           name = "Lookup Error"
289           logger.error("protocol stopping point in database didn't match names in library model")
290       return name
291
292
293   def libtags(self):
294     affs = self.tags.all().order_by('tag_name')
295     ar = []
296     for t in affs:
297       ar.append(t.__unicode__())
298     return u'%s' % ( ", ".join(ar))
299
300   def DataRun(self):
301     str ='<a target=_self href="/admin/experiments/datarun/?q='+self.id+'" title="Check All Data Runs for This Specific Library ..." ">Data Run</a>'
302     return str
303   DataRun.allow_tags = True
304
305   def aligned_m_reads(self):
306     return getLibReads(self.id)
307
308   def aligned_reads(self):
309     res = getLibReads(self.id)
310
311     # Check data sanity
312     if res[2] != "OK":
313       return u'<div style="border:solid red 2px">'+res[2]+'</div>'
314
315     rc = "%1.2f" % (res[1]/1000000.0)
316     # 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
317     if res[0] > 0:
318       bgcolor = '#ff3300'  # Red
319       rc_thr = [10000000,5000000,3000000]
320       if self.experiment_type == 'RNA-seq':
321         rc_thr = [20000000,10000000,6000000]
322
323       if res[1] > rc_thr[0]:
324         bgcolor = '#66ff66'  # Green
325       else:
326         if res[1] > rc_thr[1]:
327           bgcolor ='#00ccff'  # Blue
328         else:
329            if res[1] > rc_thr[2]:
330              bgcolor ='#ffcc33'  # Orange
331       tstr = '<div style="background-color:'+bgcolor+';color:black">'
332       tstr += res[0].__unicode__()+' Lanes, '+rc+' M Reads'
333       tstr += '</div>'
334     else: tstr = 'not processed yet'
335     return tstr
336   aligned_reads.allow_tags = True
337
338   def public(self):
339     SITE_ROOT = '/'
340     summary_url = self.get_absolute_url()
341     return '<a href="%s">S</a>' % (summary_url,)
342   public.allow_tags = True
343
344   @models.permalink
345   def get_absolute_url(self):
346     return ('htsworkflow.frontend.samples.views.library_to_flowcells', [str(self.id)])
347
348   def get_admin_url(self):
349       return urlresolvers.reverse('admin:samples_library_change',
350                                   args=(self.id,))
351
352 class HTSUser(User):
353     """
354     Provide some site-specific customization for the django user class
355     """
356     #objects = UserManager()
357
358     class Meta:
359         ordering = ['first_name', 'last_name', 'username']
360
361     def admin_url(self):
362         return '/admin/%s/%s/%d' % (self._meta.app_label, self._meta.module_name, self.id)
363
364     def __unicode__(self):
365         #return unicode(self.username) + u" (" + unicode(self.get_full_name()) + u")"
366         return unicode(self.get_full_name()) + u' (' + unicode(self.username) + ')'
367
368 def HTSUserInsertID(sender, instance, **kwargs):
369     """
370     Force addition of HTSUsers when someone just modifies the auth_user object
371     """
372     u = HTSUser.objects.filter(pk=instance.id)
373     if len(u) == 0:
374         cursor = connection.cursor()
375         cursor.execute('INSERT INTO samples_htsuser (user_ptr_id) VALUES (%s);' % (instance.id,))
376         cursor.close()
377
378 post_save.connect(HTSUserInsertID, sender=User)