Increase size of Library.multiplex_id field
[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     @models.permalink
168     def get_absolute_url(self):
169         return ('samples.views.species', [str(self.id)])
170
171
172 class Affiliation(models.Model):
173     name = models.CharField(max_length=256, db_index=True, verbose_name='Name')
174     contact = models.CharField(max_length=256,
175                                null=True, blank=True, verbose_name='Lab Name')
176     email = models.EmailField(null=True, blank=True)
177     users = models.ManyToManyField('HTSUser', null=True, blank=True)
178     users.admin_order_field = "username"
179
180     def __str__(self):
181         name = str(self.name)
182         if self.contact is not None and len(self.contact) > 0:
183             name += ' ('+self.contact+')'
184         return name
185
186     def Users(self):
187         users = self.users.all().order_by('username')
188         return ", ".join([str(a) for a in users])
189
190     class Meta:
191         ordering = ["name", "contact"]
192         unique_together = (("name", "contact"),)
193
194
195 class LibraryType(models.Model):
196     name = models.CharField(max_length=255, unique=True,
197                             verbose_name="Adapter Type")
198     is_paired_end = models.BooleanField(
199         default=True,
200         help_text="can you do a paired end run with this adapter")
201     can_multiplex = models.BooleanField(
202         default=True,
203         help_text="Does this adapter provide multiplexing?")
204
205     def __str__(self):
206         return str(self.name)
207
208     class Meta:
209         ordering = ["-id"]
210
211
212 class MultiplexIndex(models.Model):
213     """Map adapter types to the multiplex sequence"""
214     adapter_type = models.ForeignKey(LibraryType)
215     multiplex_id = models.CharField(max_length=6, null=False)
216     sequence = models.CharField(max_length=12, blank=True, null=True)
217
218     class Meta:
219         verbose_name_plural = "multiplex indicies"
220         unique_together = ('adapter_type', 'multiplex_id')
221
222
223 class Library(models.Model):
224     id = models.CharField(max_length=10, primary_key=True)
225     library_name = models.CharField(max_length=100, unique=True)
226     library_species = models.ForeignKey(Species)
227     hidden = models.BooleanField(default=False)
228     account_number = models.CharField(max_length=100, null=True, blank=True)
229     cell_line = models.ForeignKey(Cellline, blank=True, null=True,
230                                   verbose_name="Background")
231     condition = models.ForeignKey(Condition, blank=True, null=True)
232     antibody = models.ForeignKey(Antibody, blank=True, null=True)
233     affiliations = models.ManyToManyField(
234         Affiliation, related_name='library_affiliations', null=True)
235     tags = models.ManyToManyField(Tag, related_name='library_tags',
236                                   blank=True, null=True)
237     REPLICATE_NUM = [(x, x) for x in range(1, 7)]
238     replicate = models.PositiveSmallIntegerField(choices=REPLICATE_NUM,
239                                                  blank=True, null=True)
240     experiment_type = models.ForeignKey(ExperimentType)
241     library_type = models.ForeignKey(LibraryType, blank=True, null=True,
242                                      verbose_name="Adapter Type")
243     multiplex_id = models.CharField(max_length=255,
244                                     blank=True, null=True,
245                                     verbose_name="Index ID")
246     creation_date = models.DateField(blank=True, null=True)
247     made_for = models.CharField(max_length=50, blank=True,
248                                 verbose_name='ChIP/DNA/RNA Made By')
249     made_by = models.CharField(max_length=50, blank=True, default="Lorian")
250
251     PROTOCOL_END_POINTS = (
252         ('?', 'Unknown'),
253         ('Sample', 'Raw sample'),
254         ('Progress', 'In progress'),
255         ('1A', 'Ligation, then gel'),
256         ('PCR', 'Ligation, then PCR'),
257         ('1Ab', 'Ligation, PCR, then gel'),
258         ('1Ac', 'Ligation, gel, then 12x PCR'),
259         ('1Aa', 'Ligation, gel, then 18x PCR'),
260         ('2A', 'Ligation, PCR, gel, PCR'),
261         ('Done', 'Completed'),
262     )
263     PROTOCOL_END_POINTS_DICT = dict(PROTOCOL_END_POINTS)
264     stopping_point = models.CharField(max_length=25,
265                                       choices=PROTOCOL_END_POINTS,
266                                       default='Done')
267
268     amplified_from_sample = models.ForeignKey(
269         'self',
270         related_name='amplified_into_sample',
271         blank=True, null=True)
272
273     undiluted_concentration = models.DecimalField(
274         "Concentration",
275         max_digits=5, decimal_places=2, blank=True, null=True,
276         help_text="Undiluted concentration (ng/\u00b5l)")
277     # note \u00b5 is the micro symbol in unicode
278     successful_pM = models.DecimalField(
279         max_digits=9, decimal_places=1, blank=True, null=True)
280     ten_nM_dilution = models.BooleanField(default=False)
281     gel_cut_size = models.IntegerField(default=225, blank=True, null=True)
282     insert_size = models.IntegerField(blank=True, null=True)
283     notes = models.TextField(blank=True)
284
285     bioanalyzer_summary = models.TextField(blank=True, default="")
286     bioanalyzer_concentration = models.DecimalField(
287         max_digits=5, decimal_places=2, blank=True, null=True,
288         help_text="(ng/\u00b5l)")
289     bioanalyzer_image_url = models.URLField(blank=True, default="")
290
291     def __str__(self):
292         return '#%s: %s' % (self.id, self.library_name)
293
294     class Meta:
295         verbose_name_plural = "libraries"
296         # ordering = ["-creation_date"]
297         ordering = ["-id"]
298
299     def antibody_name(self):
300         str = '<a target=_self href="/admin/samples/antibody/' + \
301               self.antibody.id.__str__() + \
302               '/" title="' + self.antibody.__str__() + '">' + \
303               self.antibody.label+'</a>'
304         return str
305     antibody_name.allow_tags = True
306
307     def organism(self):
308         return self.library_species.common_name
309
310     def index_sequences(self):
311         """Return a dictionary of multiplex index id to sequence
312         Return None if the library can't multiplex,
313         """
314         if self.library_type is None:
315             return None
316         if not self.library_type.can_multiplex:
317             return None
318         if self.multiplex_id is None or len(self.multiplex_id) == 0:
319             return 'Err: id empty'
320         sequences = {}
321         multiplex_expressions = self.multiplex_id.split(',')
322         for multiplex_term in multiplex_expressions:
323             pairs = multiplex_term.split('-')
324             if len(pairs) == 1:
325                 key = pairs[0]
326                 seq = self._lookup_index(pairs[0])
327             elif len(pairs) == 2:
328                 key = pairs[0] + '-' + pairs[1]
329                 seq0 = self._lookup_index(pairs[0])
330                 seq1 = self._lookup_index(pairs[1])
331                 if seq0 is None or seq1 is None:
332                     seq = None
333                 else:
334                     seq = seq0 + '-' + seq1
335             else:
336                 raise RuntimeError("Too many - seperated sequences")
337             if seq is None:
338                 seq = 'Err: index not found'
339             sequences[key] = seq
340         return sequences
341
342     def _lookup_index(self, multiplex_id):
343         try:
344             multiplex = MultiplexIndex.objects.get(
345                 adapter_type=self.library_type.id,
346                 multiplex_id=multiplex_id)
347             return multiplex.sequence
348         except MultiplexIndex.DoesNotExist as e:
349             return None
350
351     def index_sequence_text(self, seperator=' '):
352         """Return formatted multiplex index sequences"""
353         sequences = self.index_sequences()
354         if sequences is None:
355             return ""
356         if isinstance(sequences, six.string_types):
357             return sequences
358         multiplex_ids = sorted(sequences)
359         return seperator.join(
360             ("%s:%s" % (i, sequences[i]) for i in multiplex_ids))
361     index_sequence_text.short_description = "Index"
362
363     def affiliation(self):
364         affs = self.affiliations.all().order_by('name')
365         tstr = ''
366         ar = []
367         for t in affs:
368             ar.append(t.__str__())
369         return '%s' % (", ".join(ar))
370
371     def is_archived(self):
372         """returns True if archived else False
373         """
374         if self.longtermstorage_set.count() > 0:
375             return True
376         else:
377             return False
378
379     def lanes_sequenced(self):
380         """Count how many lanes of each type were run.
381         """
382         single = 0
383         paired = 1
384         short_read = 0
385         medium_read = 1
386         long_read = 2
387         counts = [[0, 0, 0], [0, 0, 0]]
388
389         for lane in self.lane_set.all():
390             if lane.flowcell.paired_end:
391                 lane_type = paired
392             else:
393                 lane_type = single
394
395             if lane.flowcell.read_length < 40:
396                 read_type = short_read
397             elif lane.flowcell.read_length < 100:
398                 read_type = medium_read
399             else:
400                 read_type = long_read
401             counts[lane_type][read_type] += 1
402
403         return counts
404
405     def stopping_point_name(self):
406         end_points = Library.PROTOCOL_END_POINTS_DICT
407         name = end_points.get(self.stopping_point, None)
408         if name is None:
409             name = "Lookup Error"
410             logger.error("protocol stopping point in database"
411                          "didn't match names in library model")
412         return name
413
414     def libtags(self):
415         affs = self.tags.all().order_by('tag_name')
416         ar = []
417         for t in affs:
418             ar.append(t.__str__())
419         return '%s' % (", ".join(ar))
420
421     def DataRun(self):
422         str = '<a target=_self href="/admin/experiments/datarun/?q=' + \
423               self.id + \
424               '" title="Check All Data Runs for This Specific Library ..."' \
425               '">Data Run</a>'
426         return str
427     DataRun.allow_tags = True
428
429     def aligned_m_reads(self):
430         return getLibReads(self.id)
431
432     def aligned_reads(self):
433         res = getLibReads(self.id)
434
435         # Check data sanity
436         if res[2] != "OK":
437             return '<div style="border:solid red 2px">'+res[2]+'</div>'
438
439         rc = "%1.2f" % (res[1]/1000000.0)
440         # 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
441         if res[0] > 0:
442             bgcolor = '#ff3300'  # Red
443             rc_thr = [10000000, 5000000, 3000000]
444             if self.experiment_type == 'RNA-seq':
445                 rc_thr = [20000000, 10000000, 6000000]
446
447             if res[1] > rc_thr[0]:
448                 bgcolor = '#66ff66'  # Green
449             else:
450                 if res[1] > rc_thr[1]:
451                     bgcolor = '#00ccff'  # Blue
452                 else:
453                     if res[1] > rc_thr[2]:
454                         bgcolor = '#ffcc33'  # Orange
455             tstr = '<div style="background-color:'+bgcolor+';color:black">'
456             tstr += res[0].__str__()+' Lanes, '+rc+' M Reads'
457             tstr += '</div>'
458         else:
459             tstr = 'not processed yet'
460         return tstr
461     aligned_reads.allow_tags = True
462
463     def public(self):
464         summary_url = self.get_absolute_url()
465         return '<a href="%s">S</a>' % (summary_url,)
466     public.allow_tags = True
467
468     @models.permalink
469     def get_absolute_url(self):
470         return ('samples.views.library_to_flowcells', [str(self.id)])
471
472     def get_admin_url(self):
473         return urlresolvers.reverse('admin:samples_library_change',
474                                     args=(self.id,))
475
476
477 class HTSUser(User):
478     """
479     Provide some site-specific customization for the django user class
480     """
481     # objects = UserManager()
482
483     class Meta:
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)