Merge branch 'master' of mus.cacr.caltech.edu:htsworkflow
[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=6, 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 = [(x,x) for x in range(1,7)]
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_expressions = self.multiplex_id.split(',')
248       for multiplex_term in multiplex_expressions:
249           pairs = multiplex_term.split('-')
250           if len(pairs) == 1:
251               key = pairs[0]
252               seq = self._lookup_index(pairs[0])
253           elif len(pairs) == 2:
254               key = pairs[0] + '-' + pairs[1]
255               seq0 = self._lookup_index(pairs[0])
256               seq1 = self._lookup_index(pairs[1])
257               if seq0 is None or seq1 is None:
258                   seq = None
259               else:
260                   seq = seq0 + '-' + seq1
261           else:
262               raise RuntimeError("Too many - seperated sequences")
263           if seq is None:
264               seq = 'Err: index not found'
265           sequences[key] = seq
266       return sequences
267
268   def _lookup_index(self, multiplex_id):
269       try:
270           multiplex = MultiplexIndex.objects.get(
271               adapter_type = self.library_type.id,
272               multiplex_id = multiplex_id)
273           return multiplex.sequence
274       except MultiplexIndex.DoesNotExist, e:
275           return None
276
277   def index_sequence_text(self, seperator=' '):
278       """Return formatted multiplex index sequences"""
279       sequences = self.index_sequences()
280       if sequences is None:
281           return ""
282       multiplex_ids = sequences.keys()
283       multiplex_ids.sort()
284       return seperator.join(( "%s:%s" %(i,sequences[i]) for i in multiplex_ids))
285   index_sequence_text.short_description = "Index"
286
287
288   def affiliation(self):
289     affs = self.affiliations.all().order_by('name')
290     tstr = ''
291     ar = []
292     for t in affs:
293         ar.append(t.__unicode__())
294     return '%s' % (", ".join(ar))
295
296   def is_archived(self):
297     """
298     returns True if archived else False
299     """
300     if self.longtermstorage_set.count() > 0:
301         return True
302     else:
303         return False
304
305   def stopping_point_name(self):
306       end_points = Library.PROTOCOL_END_POINTS_DICT
307       name = end_points.get(self.stopping_point, None)
308       if name is None:
309           name = "Lookup Error"
310           logger.error("protocol stopping point in database didn't match names in library model")
311       return name
312
313
314   def libtags(self):
315     affs = self.tags.all().order_by('tag_name')
316     ar = []
317     for t in affs:
318       ar.append(t.__unicode__())
319     return u'%s' % ( ", ".join(ar))
320
321   def DataRun(self):
322     str ='<a target=_self href="/admin/experiments/datarun/?q='+self.id+'" title="Check All Data Runs for This Specific Library ..." ">Data Run</a>'
323     return str
324   DataRun.allow_tags = True
325
326   def aligned_m_reads(self):
327     return getLibReads(self.id)
328
329   def aligned_reads(self):
330     res = getLibReads(self.id)
331
332     # Check data sanity
333     if res[2] != "OK":
334       return u'<div style="border:solid red 2px">'+res[2]+'</div>'
335
336     rc = "%1.2f" % (res[1]/1000000.0)
337     # 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
338     if res[0] > 0:
339       bgcolor = '#ff3300'  # Red
340       rc_thr = [10000000,5000000,3000000]
341       if self.experiment_type == 'RNA-seq':
342         rc_thr = [20000000,10000000,6000000]
343
344       if res[1] > rc_thr[0]:
345         bgcolor = '#66ff66'  # Green
346       else:
347         if res[1] > rc_thr[1]:
348           bgcolor ='#00ccff'  # Blue
349         else:
350            if res[1] > rc_thr[2]:
351              bgcolor ='#ffcc33'  # Orange
352       tstr = '<div style="background-color:'+bgcolor+';color:black">'
353       tstr += res[0].__unicode__()+' Lanes, '+rc+' M Reads'
354       tstr += '</div>'
355     else: tstr = 'not processed yet'
356     return tstr
357   aligned_reads.allow_tags = True
358
359   def public(self):
360     SITE_ROOT = '/'
361     summary_url = self.get_absolute_url()
362     return '<a href="%s">S</a>' % (summary_url,)
363   public.allow_tags = True
364
365   @models.permalink
366   def get_absolute_url(self):
367     return ('htsworkflow.frontend.samples.views.library_to_flowcells', [str(self.id)])
368
369   def get_admin_url(self):
370       return urlresolvers.reverse('admin:samples_library_change',
371                                   args=(self.id,))
372
373 class HTSUser(User):
374     """
375     Provide some site-specific customization for the django user class
376     """
377     #objects = UserManager()
378
379     class Meta:
380         ordering = ['first_name', 'last_name', 'username']
381
382     def admin_url(self):
383         return '/admin/%s/%s/%d' % (self._meta.app_label, self._meta.module_name, self.id)
384
385     def __unicode__(self):
386         #return unicode(self.username) + u" (" + unicode(self.get_full_name()) + u")"
387         return unicode(self.get_full_name()) + u' (' + unicode(self.username) + ')'
388
389 def HTSUserInsertID(sender, instance, **kwargs):
390     """
391     Force addition of HTSUsers when someone just modifies the auth_user object
392     """
393     u = HTSUser.objects.filter(pk=instance.id)
394     if len(u) == 0:
395         cursor = connection.cursor()
396         cursor.execute('INSERT INTO samples_htsuser (user_ptr_id) VALUES (%s);' % (instance.id,))
397         cursor.close()
398
399 post_save.connect(HTSUserInsertID, sender=User)