| 1 | from django.db import models, IntegrityError |
|---|
| 2 | |
|---|
| 3 | from django.utils.translation import ugettext_lazy as _ |
|---|
| 4 | |
|---|
| 5 | from datetime import datetime |
|---|
| 6 | import string |
|---|
| 7 | from random import choice |
|---|
| 8 | from time import sleep |
|---|
| 9 | |
|---|
| 10 | from utilities import safe_truncate |
|---|
| 11 | |
|---|
| 12 | from vocabulary.models import CountryCode, StudyPhase, StudyType, RecruitmentStatus |
|---|
| 13 | from vocabulary.models import InterventionCode, TrialNumberIssuingAuthority |
|---|
| 14 | |
|---|
| 15 | from registry import choices |
|---|
| 16 | |
|---|
| 17 | # remove digits that look like letters and vice-versa |
|---|
| 18 | # remove vowels to avoid forming words |
|---|
| 19 | BASE28 = ''.join(d for d in string.digits+string.ascii_lowercase |
|---|
| 20 | if d not in '1l0aeiou') |
|---|
| 21 | TRIAL_ID_PREFIX = 'RBR' |
|---|
| 22 | TRIAL_ID_DIGITS = 6 |
|---|
| 23 | TRIAL_ID_TRIES = 3 |
|---|
| 24 | |
|---|
| 25 | def generate_trial_id(prefix, num_digits): |
|---|
| 26 | s = choice(string.digits) # start with a numeric digit |
|---|
| 27 | s += ''.join(choice(BASE28) for i in range(1, num_digits)) |
|---|
| 28 | return '-'.join([prefix, s[:num_digits/2], s[num_digits/2:]]) |
|---|
| 29 | |
|---|
| 30 | class ClinicalTrial(models.Model): |
|---|
| 31 | # TRDS 1 |
|---|
| 32 | trial_id = models.CharField(_('Primary Id Number'), null=True, unique=True, |
|---|
| 33 | max_length=255, editable=False) |
|---|
| 34 | # TRDS 2 |
|---|
| 35 | date_registration = models.DateField(_('Date of Registration'), null=True, |
|---|
| 36 | editable=False, db_index=True) |
|---|
| 37 | # TRDS 10a |
|---|
| 38 | scientific_title = models.TextField(_('Scientific Title'), |
|---|
| 39 | max_length=2000) |
|---|
| 40 | # TRDS 10b |
|---|
| 41 | scientific_acronym = models.CharField(_('Scientific Acronym'), blank=True, |
|---|
| 42 | max_length=255) |
|---|
| 43 | # TRDS 5 |
|---|
| 44 | primary_sponsor = models.OneToOneField('Institution', null=True, blank=True, |
|---|
| 45 | verbose_name=_('Primary Sponsor')) |
|---|
| 46 | # TRDS 9a |
|---|
| 47 | public_title = models.TextField(_('Public Title'), blank=True, |
|---|
| 48 | max_length=2000) |
|---|
| 49 | # TRDS 9b |
|---|
| 50 | acronym = models.CharField(_('Acronym'), blank=True, max_length=255) |
|---|
| 51 | |
|---|
| 52 | # TRDS 12a |
|---|
| 53 | hc_freetext = models.TextField(_('Health Condition(s)'), blank=True, |
|---|
| 54 | max_length=8000) |
|---|
| 55 | # TRDS 13a |
|---|
| 56 | i_freetext = models.TextField(_('Intervention(s)'), blank=True, |
|---|
| 57 | max_length=8000) |
|---|
| 58 | # TRDS 14a |
|---|
| 59 | inclusion_criteria = models.TextField(_('Inclusion Criteria'), blank=True, |
|---|
| 60 | max_length=8000) |
|---|
| 61 | # TRDS 14b |
|---|
| 62 | gender = models.CharField(_('Gender (inclusion sex)'), max_length=1, |
|---|
| 63 | choices=choices.INCLUSION_GENDER, |
|---|
| 64 | default=choices.INCLUSION_GENDER[0][0]) |
|---|
| 65 | # TRDS 14c |
|---|
| 66 | agemin_value = models.PositiveIntegerField(_('Inclusion Minimum Age'), |
|---|
| 67 | default=0) |
|---|
| 68 | agemin_unit = models.CharField(_('Minimum Age Unit'), max_length=1, |
|---|
| 69 | choices=choices.INCLUSION_AGE_UNIT, |
|---|
| 70 | default=choices.INCLUSION_AGE_UNIT[0][0]) |
|---|
| 71 | # TRDS 14d |
|---|
| 72 | agemax_value = models.PositiveIntegerField(_('Inclusion Maximum Age'), |
|---|
| 73 | default=0) |
|---|
| 74 | agemax_unit = models.CharField(_('Maximum Age Unit'), max_length=1, |
|---|
| 75 | choices=choices.INCLUSION_AGE_UNIT, |
|---|
| 76 | default=choices.INCLUSION_AGE_UNIT[0][0]) |
|---|
| 77 | # TRDS 14e |
|---|
| 78 | exclusion_criteria = models.TextField(_('Exclusion Criteria'), blank=True, |
|---|
| 79 | max_length=8000) |
|---|
| 80 | # TRDS 15a |
|---|
| 81 | study_type = models.ForeignKey(StudyType, null=True, blank=True, |
|---|
| 82 | verbose_name=_('Study Type')) |
|---|
| 83 | |
|---|
| 84 | # TRDS 15b |
|---|
| 85 | study_design = models.TextField(_('Study Design'), blank=True, |
|---|
| 86 | max_length=1000) |
|---|
| 87 | # TRDS 15c |
|---|
| 88 | phase = models.ForeignKey(StudyPhase, null=True, blank=True, |
|---|
| 89 | verbose_name=_('Study Phase')) |
|---|
| 90 | |
|---|
| 91 | # TRDS 16a,b (type_enrollment="anticipated") |
|---|
| 92 | date_enrollment_anticipated = models.CharField( # yyyy-mm or yyyy-mm-dd |
|---|
| 93 | _('Anticipated Date of First Enrollment'), max_length=10, blank=True) |
|---|
| 94 | |
|---|
| 95 | # TRDS 16a,b (type_enrollment="actual") |
|---|
| 96 | date_enrollment_actual = models.CharField( # yyyy-mm or yyyy-mm-dd |
|---|
| 97 | _('Actual Date of First Enrollment'), max_length=10, blank=True) |
|---|
| 98 | |
|---|
| 99 | # TRDS 17 |
|---|
| 100 | target_sample_size = models.PositiveIntegerField(_('Target Sample Size'), |
|---|
| 101 | default=0) |
|---|
| 102 | # TRDS 18 |
|---|
| 103 | recruitment_status = models.ForeignKey(RecruitmentStatus, null=True, blank=True, |
|---|
| 104 | verbose_name=_('Recruitment Status')) |
|---|
| 105 | |
|---|
| 106 | ################################### internal use, administrative fields ### |
|---|
| 107 | created = models.DateTimeField(default=datetime.now, editable=False) |
|---|
| 108 | updated = models.DateTimeField(_('Last Update'), null=True, editable=False) |
|---|
| 109 | exported = models.DateTimeField(null=True, editable=False) |
|---|
| 110 | status = models.CharField(_('Status'), max_length=64, |
|---|
| 111 | choices=choices.TRIAL_RECORD_STATUS, |
|---|
| 112 | default=choices.TRIAL_RECORD_STATUS[0][0]) |
|---|
| 113 | staff_note = models.CharField(_('Record Note (staff use only)'), |
|---|
| 114 | max_length='255', |
|---|
| 115 | blank=True) |
|---|
| 116 | |
|---|
| 117 | class Meta: |
|---|
| 118 | ordering = ['-updated',] |
|---|
| 119 | |
|---|
| 120 | def save(self): |
|---|
| 121 | if self.id: |
|---|
| 122 | self.updated = datetime.now() |
|---|
| 123 | if self.status == choices.PUBLISHED_STATUS and not self.trial_id: |
|---|
| 124 | for i in range(TRIAL_ID_TRIES): |
|---|
| 125 | self.trial_id = generate_trial_id(TRIAL_ID_PREFIX, TRIAL_ID_DIGITS) |
|---|
| 126 | try: |
|---|
| 127 | super(ClinicalTrial, self).save() |
|---|
| 128 | except IntegrityError: |
|---|
| 129 | if i < TRIAL_ID_TRIES: |
|---|
| 130 | sleep(2**i) # wait to try again |
|---|
| 131 | else: |
|---|
| 132 | raise # all tries exhausted: give up |
|---|
| 133 | else: |
|---|
| 134 | break # no need to try again |
|---|
| 135 | else: |
|---|
| 136 | super(ClinicalTrial, self).save() |
|---|
| 137 | |
|---|
| 138 | def identifier(self): |
|---|
| 139 | return self.trial_id or '(req:%s)' % self.pk |
|---|
| 140 | |
|---|
| 141 | def short_title(self): |
|---|
| 142 | if self.scientific_acronym: |
|---|
| 143 | tit = u'%s - %s' % (self.scientific_acronym, |
|---|
| 144 | self.scientific_title) |
|---|
| 145 | else: |
|---|
| 146 | tit = self.scientific_title |
|---|
| 147 | return safe_truncate(tit, 120) |
|---|
| 148 | |
|---|
| 149 | def __unicode__(self): |
|---|
| 150 | return u'%s %s' % (self.identifier(), self.short_title()) |
|---|
| 151 | |
|---|
| 152 | def trial_id_display(self): |
|---|
| 153 | ''' return the trial id or an explicit message it is None ''' |
|---|
| 154 | if self.trial_id: |
|---|
| 155 | return self.trial_id |
|---|
| 156 | else: |
|---|
| 157 | msg = 'not assigned (request #%)' % self.pk |
|---|
| 158 | |
|---|
| 159 | def record_status(self): |
|---|
| 160 | return self.submission.status |
|---|
| 161 | |
|---|
| 162 | #TRDS 3 - Secondarty ID Numbers |
|---|
| 163 | def trial_number(self): |
|---|
| 164 | return self.trialnumber_set.all().select_related(); |
|---|
| 165 | |
|---|
| 166 | # TRDS 4 - Source(s) of Monetary Support |
|---|
| 167 | def support_sources(self): |
|---|
| 168 | return self.trialsupportsource_set.all() |
|---|
| 169 | |
|---|
| 170 | # TRDS 6 - Secondary Sponsor(s) |
|---|
| 171 | def secondary_sponsors(self): |
|---|
| 172 | return self.trialsecondarysponsor_set.all() |
|---|
| 173 | |
|---|
| 174 | def updated_str(self): |
|---|
| 175 | return self.updated.strftime('%Y-%m-%d %H:%M') |
|---|
| 176 | updated_str.short_description = _('Updated') |
|---|
| 177 | |
|---|
| 178 | def related_contacts(self, relation): |
|---|
| 179 | ''' return set of Contacts related to this trial with a |
|---|
| 180 | given relationship |
|---|
| 181 | ''' |
|---|
| 182 | return (r.contact for r in |
|---|
| 183 | self.trialcontact_set.filter(relation=relation).select_related()) |
|---|
| 184 | |
|---|
| 185 | def related_health_conditions(self, aspect, level): |
|---|
| 186 | ''' return set of hc-code or keywords related to this trial with a |
|---|
| 187 | given relationship |
|---|
| 188 | ''' |
|---|
| 189 | return self.descriptor_set.filter(aspect=aspect, level=level).select_related() |
|---|
| 190 | |
|---|
| 191 | |
|---|
| 192 | # TRDS 7 - Contact for Public Queries |
|---|
| 193 | |
|---|
| 194 | def public_contacts(self): |
|---|
| 195 | ''' return set of Contacts related to this trial with |
|---|
| 196 | relation='PublicContact' |
|---|
| 197 | ''' |
|---|
| 198 | return self.related_contacts('PublicContact') |
|---|
| 199 | |
|---|
| 200 | # TRDS 8 - Contact for Scientific Queries |
|---|
| 201 | |
|---|
| 202 | def scientific_contacts(self): |
|---|
| 203 | ''' return set of Contacts related to this trial with |
|---|
| 204 | relation='ScientificContact' |
|---|
| 205 | ''' |
|---|
| 206 | return self.related_contacts('ScientificContact') |
|---|
| 207 | |
|---|
| 208 | #TRDS 12b - HC-CODE |
|---|
| 209 | def hc_code(self): |
|---|
| 210 | ''' return set of HC-Code related to this trial with |
|---|
| 211 | aspect = 'HealthCondition' |
|---|
| 212 | level = 'general' |
|---|
| 213 | ''' |
|---|
| 214 | return self.related_health_conditions('HealthCondition','general') |
|---|
| 215 | |
|---|
| 216 | #TRDS 12c - HC-Keyword |
|---|
| 217 | def hc_keyword(self): |
|---|
| 218 | ''' return set of HC-Code related to this trial with |
|---|
| 219 | aspect = 'HealthCondition' |
|---|
| 220 | level = 'specific' |
|---|
| 221 | ''' |
|---|
| 222 | return self.related_health_conditions('HealthCondition','specific') |
|---|
| 223 | |
|---|
| 224 | #TRDS 13b - Invetion Code |
|---|
| 225 | def intervention_code(self): |
|---|
| 226 | ''' return set of Intervention Code related to this trial with |
|---|
| 227 | ''' |
|---|
| 228 | return (r.i_code for r in |
|---|
| 229 | self.trialinterventioncode_set.all().select_related()) |
|---|
| 230 | |
|---|
| 231 | #TRDS 13c - Invention Keyword |
|---|
| 232 | def intervention_keyword(self): |
|---|
| 233 | ''' return set of Intervention Keyword related to this trial with |
|---|
| 234 | ''' |
|---|
| 235 | return self.descriptor_set.filter(aspect='intervention').select_related() |
|---|
| 236 | |
|---|
| 237 | #TRDS 19 - Primary Outcomes |
|---|
| 238 | def primary_outcomes(self): |
|---|
| 239 | ''' return set of Primary Outcomes related to this trial with |
|---|
| 240 | ''' |
|---|
| 241 | return self.outcome_set.filter(interest='primary').select_related() |
|---|
| 242 | |
|---|
| 243 | #TRDS 20 - Secondary Outcomes |
|---|
| 244 | def secondary_outcomes(self): |
|---|
| 245 | ''' return set of Secondary Outcomes related to this trial with |
|---|
| 246 | ''' |
|---|
| 247 | return self.outcome_set.filter(interest='secondary').select_related() |
|---|
| 248 | |
|---|
| 249 | |
|---|
| 250 | |
|---|
| 251 | ################################### Entities linked to a Clinical Trial ### |
|---|
| 252 | |
|---|
| 253 | # TRDS 3 - Secondary Identifying Numbers |
|---|
| 254 | |
|---|
| 255 | class TrialNumber(models.Model): |
|---|
| 256 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 257 | issuing_authority = models.CharField(_('Issuing Authority'), |
|---|
| 258 | max_length=255, db_index=True, |
|---|
| 259 | choices=TrialNumberIssuingAuthority.choices()) |
|---|
| 260 | id_number = models.CharField(_('Secondary Id Number'), |
|---|
| 261 | max_length=255, db_index=True) |
|---|
| 262 | |
|---|
| 263 | def __unicode__(self): |
|---|
| 264 | return u'%s: %s' % (self.issuing_authority, self.id_number) |
|---|
| 265 | |
|---|
| 266 | # TRDS 6 - Secondary Sponsor(s) |
|---|
| 267 | class TrialSecondarySponsor(models.Model): |
|---|
| 268 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 269 | institution = models.ForeignKey('Institution') |
|---|
| 270 | |
|---|
| 271 | def __unicode__(self): |
|---|
| 272 | return u'%s: %s' % (self.trial, self.institution) |
|---|
| 273 | |
|---|
| 274 | # TRDS 4 - Source(s) of Monetary Support |
|---|
| 275 | class TrialSupportSource(models.Model): |
|---|
| 276 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 277 | institution = models.ForeignKey('Institution') |
|---|
| 278 | |
|---|
| 279 | def __unicode__(self): |
|---|
| 280 | return u'%s: %s' % (self.trial, self.institution) |
|---|
| 281 | |
|---|
| 282 | # TRDS 5 - Primary Sponsor |
|---|
| 283 | |
|---|
| 284 | class Institution(models.Model): |
|---|
| 285 | name = models.CharField(_('Name'), max_length=255) |
|---|
| 286 | address = models.TextField(_('Postal Address'), max_length=1500, blank=True) |
|---|
| 287 | country = models.ForeignKey(CountryCode, verbose_name=_('Country')) |
|---|
| 288 | |
|---|
| 289 | def __unicode__(self): |
|---|
| 290 | return safe_truncate(self.name, 80) |
|---|
| 291 | |
|---|
| 292 | # TRDS 7 - Contact for Public Queries |
|---|
| 293 | # TRDS 8 - Contact for Scientific Queries |
|---|
| 294 | |
|---|
| 295 | class Contact(models.Model): |
|---|
| 296 | firstname = models.CharField(_('First Name'), max_length=50) |
|---|
| 297 | middlename = models.CharField(_('Middle Name'), max_length=50, blank=True) |
|---|
| 298 | lastname = models.CharField(_('Last Name'), max_length=50) |
|---|
| 299 | email = models.EmailField(_('E-mail'), max_length=255) |
|---|
| 300 | affiliation = models.ForeignKey(Institution, null=True, blank=True, |
|---|
| 301 | verbose_name=_('Affiliation')) |
|---|
| 302 | address = models.CharField(_('Address'), max_length=255, blank=True) |
|---|
| 303 | city = models.CharField(_('City'), max_length=255, blank=True) |
|---|
| 304 | country = models.ForeignKey(CountryCode, null=True, blank=True, |
|---|
| 305 | verbose_name=_('Country'),) |
|---|
| 306 | zip = models.CharField(_('Postal Code'), max_length=50, blank=True) |
|---|
| 307 | telephone = models.CharField(_('Telephone'), max_length=255, blank=True) |
|---|
| 308 | |
|---|
| 309 | def name(self): |
|---|
| 310 | names = self.firstname + u' ' + self.middlename + u' ' + self.lastname |
|---|
| 311 | return u' '.join(names.split()) |
|---|
| 312 | |
|---|
| 313 | def __unicode__(self): |
|---|
| 314 | return self.name() |
|---|
| 315 | |
|---|
| 316 | class TrialContact(models.Model): |
|---|
| 317 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 318 | contact = models.ForeignKey(Contact) |
|---|
| 319 | relation = models.CharField(_('Relationship'), max_length=255, |
|---|
| 320 | choices = choices.CONTACT_RELATION) |
|---|
| 321 | status = models.CharField(_('Status'), max_length=255, |
|---|
| 322 | choices = choices.CONTACT_STATUS, |
|---|
| 323 | default = choices.CONTACT_STATUS[0][0]) |
|---|
| 324 | |
|---|
| 325 | def __unicode__(self): |
|---|
| 326 | return u'%s, %s: %s (%s)' % (self.relation, self.trial.short_title(), |
|---|
| 327 | self.contact.name(), self.status) |
|---|
| 328 | |
|---|
| 329 | # TRDS 11 - Countries of Recruitment |
|---|
| 330 | |
|---|
| 331 | class RecruitmentCountry(models.Model): |
|---|
| 332 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 333 | country = models.ForeignKey(CountryCode, verbose_name=_('Country')) |
|---|
| 334 | |
|---|
| 335 | class Meta: |
|---|
| 336 | verbose_name_plural = _('Recruitment Countries') |
|---|
| 337 | |
|---|
| 338 | def __unicode__(self): |
|---|
| 339 | return self.country.description |
|---|
| 340 | |
|---|
| 341 | # TRDS 13b - Intervention(s), intervention code |
|---|
| 342 | |
|---|
| 343 | class TrialInterventionCode(models.Model): |
|---|
| 344 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 345 | i_code = models.ForeignKey(InterventionCode) |
|---|
| 346 | |
|---|
| 347 | class Meta: |
|---|
| 348 | order_with_respect_to = 'trial' |
|---|
| 349 | |
|---|
| 350 | def __unicode__(self): |
|---|
| 351 | return u'%s: %s' % (self.trial.short_title(), self.i_code.label) |
|---|
| 352 | |
|---|
| 353 | # TRDS 19 - Primary Outcome(s) |
|---|
| 354 | # TRDS 20 - Key Secondary Outcome(s) |
|---|
| 355 | |
|---|
| 356 | class Outcome(models.Model): |
|---|
| 357 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 358 | interest = models.CharField(_('Interest'), max_length=32, |
|---|
| 359 | choices=choices.OUTCOME_INTEREST, |
|---|
| 360 | default = choices.OUTCOME_INTEREST[0][0]) |
|---|
| 361 | description = models.TextField(_('Outcome Description'), max_length=8000) |
|---|
| 362 | |
|---|
| 363 | class Meta: |
|---|
| 364 | order_with_respect_to = 'trial' |
|---|
| 365 | |
|---|
| 366 | def __unicode__(self): |
|---|
| 367 | return safe_truncate(self.description, 80) |
|---|
| 368 | |
|---|
| 369 | class Descriptor(models.Model): |
|---|
| 370 | aspect = models.CharField(_('Trial Aspect'), max_length=255, |
|---|
| 371 | choices=choices.TRIAL_ASPECT) |
|---|
| 372 | vocabulary = models.CharField(_('Vocabulary'), max_length=255, |
|---|
| 373 | choices=choices.DESCRIPTOR_VOCABULARY) |
|---|
| 374 | version = models.CharField(_('Version'), max_length=64, blank=True) |
|---|
| 375 | level = models.CharField(_('Level'), max_length=64, |
|---|
| 376 | choices=choices.DESCRIPTOR_LEVEL) |
|---|
| 377 | code = models.CharField(_('Code'), max_length=255) |
|---|
| 378 | text = models.CharField(_('Text'), max_length=255, blank=True) |
|---|
| 379 | |
|---|
| 380 | def __unicode__(self): |
|---|
| 381 | return u'[%s] %s: %s' % (self.vocabulary, self.code, self.text) |
|---|
| 382 | |
|---|
| 383 | class GeneralDescriptor(models.Model): |
|---|
| 384 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 385 | descriptor = models.ForeignKey(Descriptor) |
|---|
| 386 | |
|---|
| 387 | class Meta: |
|---|
| 388 | order_with_respect_to = 'descriptor' |
|---|
| 389 | |
|---|
| 390 | def trial_identifier(self): |
|---|
| 391 | return self.trial.identifier() |
|---|
| 392 | |
|---|
| 393 | class SpecificDescriptor(models.Model): |
|---|
| 394 | trial = models.ForeignKey(ClinicalTrial) |
|---|
| 395 | descriptor = models.ForeignKey(Descriptor) |
|---|
| 396 | |
|---|
| 397 | class Meta: |
|---|
| 398 | order_with_respect_to = 'descriptor' |
|---|
| 399 | |
|---|
| 400 | def trial_identifier(self): |
|---|
| 401 | return self.trial.identifier() |
|---|