root/trunk/grails-app/domain/dbnp/studycapturing/Study.groovy @ 1857

Revision 1857, 18.6 KB (checked in by work@…, 3 years ago)

- resolves #423

  • Property svn:keywords set to Rev Author Date
Line 
1package dbnp.studycapturing
2import org.dbnp.gdt.*
3import dbnp.authentication.SecUser
4
5/**
6 * Domain class describing the basic entity in the study capture part: the Study class.
7 *
8 * Revision information:
9 * $Rev$
10 * $Author$
11 * $Date$
12 */
13class Study extends TemplateEntity {
14        static searchable = true
15
16        def moduleNotificationService
17
18        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
19        String title            // The title of the study
20        String description      // A brief synopsis of what the study is about
21        String code                     // currently used as the external study ID, e.g. to reference a study in a SAM module
22        Date dateCreated
23        Date lastUpdated
24        Date startDate
25        List subjects
26        List events
27        List samplingEvents
28        List eventGroups
29        List samples
30        List assays
31        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
32        boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
33
34        /**
35         * UUID of this study
36         */
37        String studyUUID
38
39
40        static hasMany = [
41                subjects: Subject,
42                samplingEvents: SamplingEvent,
43                events: Event,
44                eventGroups: EventGroup,
45                samples: Sample,
46                assays: Assay,
47                persons: StudyPerson,
48                publications: Publication,
49                readers: SecUser,
50                writers: SecUser
51        ]
52
53        static constraints = {
54                title(nullable:false, blank: false, unique:true, maxSize: 255)
55                owner(nullable: true, blank: true)
56                code(nullable: true, blank: true, unique: true, maxSize: 255)
57                studyUUID(nullable:true, unique:true, maxSize: 255)
58                persons(size:1..1000)
59                // TODO: add custom validator for 'published' to assess whether the study meets all quality criteria for publication
60                // tested by SampleTests.testStudyPublish
61        }
62
63        // see org.dbnp.gdt.FuzzyStringMatchController and Service
64        static fuzzyStringMatchable = [
65            "title",
66                "code"
67        ]
68
69        static mapping = {
70                autoTimestamp true
71                sort "title"
72
73                // Make sure the TEXT field description is persisted with a TEXT field in the database
74                description type: 'text'
75                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
76                templateTextFields type: 'text'
77
78        }
79
80        // The external identifier (studyToken) is currently the code of the study.
81        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
82
83        def getToken() { return giveUUID() }
84
85        /**
86         * return the domain fields for this domain class
87         * @return List
88         */
89        static List<TemplateField> giveDomainFields() { return Study.domainFields }
90
91        static final List<TemplateField> domainFields = [
92                new TemplateField(
93                name: 'title',
94                type: TemplateFieldType.STRING,
95                required: true),
96                new TemplateField(
97                name: 'description',
98                type: TemplateFieldType.TEXT,
99                comment:'Give a brief synopsis of what your study is about',
100                required: true),
101                new TemplateField(
102                name: 'code',
103                type: TemplateFieldType.STRING,
104                preferredIdentifier: true,
105                comment: 'Fill out the code by which many people will recognize your study',
106                required: false),
107                new TemplateField(
108                name: 'startDate',
109                type: TemplateFieldType.DATE,
110                comment: 'Fill out the official start date or date of first action',
111                required: true),
112                new TemplateField(
113                name: 'published',
114                type: TemplateFieldType.BOOLEAN,
115                comment: 'Determines whether this study is published (accessible for the study readers and, if the study is public, for anonymous users). A study can only be published if it meets certain quality criteria, which will be checked upon save.',
116                required: false)
117        ]
118
119        /**
120         * return the title of this study
121         */
122        def String toString() {
123                return ( (code) ? code : "[no code]") + " - "+ title
124        }
125
126        /**
127         * returns all events and sampling events that do not belong to a group
128         */
129        def List<Event> getOrphanEvents() {
130                def orphans = events.findAll { event -> !event.belongsToGroup(eventGroups) } +
131                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
132
133                return orphans
134        }
135
136        /**
137         * Return the unique Subject templates that are used in this study
138         */
139        def List<Template> giveSubjectTemplates() {
140                TemplateEntity.giveTemplates(subjects)
141        }
142
143        /**
144         * Return all subjects for a specific template
145         * @param Template
146         * @return ArrayList
147         */
148        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
149                subjects.findAll { it.template.equals(template) }
150        }
151
152        /**
153         * Return all unique assay templates
154         * @return Set
155         */
156        List<Template> giveAllAssayTemplates() {
157                TemplateEntity.giveTemplates(((assays) ? assays : []))
158        }
159
160        /**
161         * Return all assays for a particular template
162         * @return ArrayList
163         */
164        def ArrayList giveAssaysForTemplate(Template template) {
165                assays.findAll { it && it.template.equals(template) }
166        }
167
168        /**
169         * Return the unique Event and SamplingEvent templates that are used in this study
170         */
171        List<Template> giveAllEventTemplates() {
172                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
173                // gives trouble when asking .size() to the result
174                // So we also use giveTemplates here
175                TemplateEntity.giveTemplates(((events) ? events : []) + ((samplingEvents) ? samplingEvents : []))
176        }
177
178        /**
179         * Return all events and samplingEvenets for a specific template
180         * @param Template
181         * @return ArrayList
182         */
183        def ArrayList giveEventsForTemplate(Template template) {
184                def events = events.findAll { it.template.equals(template) }
185                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
186
187                return (events) ? events : samplingEvents
188        }
189
190        /**
191         * Return the unique Event templates that are used in this study
192         */
193        List<Template> giveEventTemplates() {
194                TemplateEntity.giveTemplates(events)
195        }
196
197        /**
198         * Return the unique SamplingEvent templates that are used in this study
199         */
200        List<Template> giveSamplingEventTemplates() {
201                TemplateEntity.giveTemplates(samplingEvents)
202        }
203
204        /**
205         * Returns the unique Sample templates that are used in the study
206         */
207        List<Template> giveSampleTemplates() {
208                TemplateEntity.giveTemplates(samples)
209        }
210
211        /**
212         * Return all samples for a specific template, sorted by subject name
213         * @param Template
214         * @return ArrayList
215         */
216        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
217                samples.findAll { it.template.equals(template) }.sort {
218                        [it.parentEvent?.template, it.parentEvent?.startTime, it.parentSubject?.name]
219                }
220        }
221
222        /**
223         * Returns the template of the study
224         */
225        Template giveStudyTemplate() {
226                return this.template
227        }
228
229        /**
230         * Delete a specific subject from this study, including all its relations
231         * @param subject The subject to be deleted
232         * @void
233         */
234        void deleteSubject(Subject subject) {
235                // Delete the subject from the event groups it was referenced in
236                this.eventGroups.each {
237                        if (it.subjects?.contains(subject)) {
238                                it.removeFromSubjects(subject)
239                        }
240                }
241
242                // Delete the samples that have this subject as parent
243                this.samples.findAll { it.parentSubject.equals(subject) }.each {
244                        this.deleteSample(it)
245                }
246
247                // This should remove the subject itself too, because of the cascading belongsTo relation
248                this.removeFromSubjects(subject)
249
250                // But apparently it needs an explicit delete() too
251                subject.delete()
252        }
253
254        /**
255         * Delete an assay from the study
256         * @param Assay
257         * @void
258         */
259        def deleteAssay(Assay assay) {
260                if (assay && assay instanceof Assay) {
261                        // iterate through linked samples
262                        assay.samples.findAll { true }.each() { sample ->
263                                assay.removeFromSamples(sample)
264                        }
265
266                        // remove this assay from the study
267                        this.removeFromAssays(assay)
268
269                        // and delete it explicitly
270                        assay.delete()
271                }
272        }
273
274        /**
275         * Delete an event from the study, including all its relations
276         * @param Event
277         * @void
278         */
279        void deleteEvent(Event event) {
280                // remove event from eventGroups
281                this.eventGroups.each() { eventGroup ->
282                        eventGroup.removeFromEvents(event)
283                }
284
285                // remove event from the study
286                this.removeFromEvents(event)
287
288                // and perform a hard delete
289                event.delete()
290        }
291
292        /**
293         * Delete a sample from the study, including all its relations
294         * @param Event
295         * @void
296         */
297        void deleteSample(Sample sample) {
298                // remove the sample from the study
299                this.removeFromSamples(sample)
300
301                // remove the sample from any sampling events it belongs to
302                this.samplingEvents.findAll { it.samples.any { it == sample }}.each {
303                        it.removeFromSamples(sample)
304                }
305
306                // remove the sample from any assays it belongs to
307                this.assays.findAll { it.samples.any { it == sample }}.each {
308                        it.removeFromSamples(sample)
309                }
310
311                // Also here, contrary to documentation, an extra delete() is needed
312                // otherwise date is not properly deleted!
313                sample.delete()
314        }
315
316        /**
317         * Delete a samplingEvent from the study, including all its relations
318         * @param SamplingEvent
319         * @void
320         */
321        void deleteSamplingEvent(SamplingEvent samplingEvent) {
322                // remove event from eventGroups
323                this.eventGroups.each() { eventGroup ->
324                        eventGroup.removeFromSamplingEvents(samplingEvent)
325                }
326
327                // Delete the samples that have this sampling event as parent
328                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
329                        // This should remove the sample itself too, because of the cascading belongsTo relation
330                        this.deleteSample(it)
331                }
332
333                // Remove event from the study
334                // This should remove the event group itself too, because of the cascading belongsTo relation
335                this.removeFromSamplingEvents(samplingEvent)
336
337                // But apparently it needs an explicit delete() too
338                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
339                samplingEvent.delete()
340        }
341
342        /**
343         * Delete an eventGroup from the study, including all its relations
344         * @param EventGroup
345         * @void
346         */
347        void deleteEventGroup(EventGroup eventGroup) {
348                // If the event group contains sampling events
349                if (eventGroup.samplingEvents) {
350                        // remove all samples that originate from this eventGroup
351                        if (eventGroup.samplingEvents.size()) {
352                                // find all samples related to this eventGroup
353                                // - subject comparison is relatively straightforward and
354                                //   behaves as expected
355                                // - event comparison behaves strange, so now we compare
356                                //              1. database id's or,
357                                //              2. object identifiers or,
358                                //              3. objects itself
359                                //   this seems now to work as expected
360                                this.samples.findAll { sample ->
361                                        (
362                                                        (eventGroup.subjects.findAll {
363                                                                it.equals(sample.parentSubject)
364                                                        })
365                                                        &&
366                                                        (eventGroup.samplingEvents.findAll {
367                                                                (
368                                                                                (it.id && sample.parentEvent.id && it.id == sample.parentEvent.id)
369                                                                                ||
370                                                                                (it.getIdentifier() == sample.parentEvent.getIdentifier())
371                                                                                ||
372                                                                                it.equals(sample.parentEvent)
373                                                                                )
374                                                        })
375                                                        )
376                                }.each() { sample ->
377                                        // remove sample from study
378                                        this.deleteSample(sample)
379                                }
380                        }
381
382                        // remove all samplingEvents from this eventGroup
383                        eventGroup.samplingEvents.findAll {}.each() {
384                                eventGroup.removeFromSamplingEvents(it)
385                        }
386                }
387
388                // If the event group contains subjects
389                if (eventGroup.subjects) {
390                        // remove all subject from this eventGroup
391                        eventGroup.subjects.findAll {}.each() {
392                                eventGroup.removeFromSubjects(it)
393                        }
394                }
395
396                // remove the eventGroup from the study
397                this.removeFromEventGroups(eventGroup)
398
399                // Also here, contrary to documentation, an extra delete() is needed
400                // otherwise cascaded deletes are not properly performed
401                eventGroup.delete()
402        }
403
404        /**
405         * Returns true if the given user is allowed to read this study
406         */
407        public boolean canRead(SecUser loggedInUser) {
408                // Anonymous readers are only given access when published and public
409                if (loggedInUser == null) {
410                        return this.publicstudy && this.published;
411                }
412
413                // Administrators are allowed to read every study
414                if (loggedInUser.hasAdminRights()) {
415                        return true;
416                }
417
418                // Owners and writers are allowed to read this study
419                if (this.owner == loggedInUser || this.writers.contains(loggedInUser)) {
420                        return true
421                }
422
423                // Readers are allowed to read this study when it is published
424                if (this.readers.contains(loggedInUser) && this.published) {
425                        return true
426                }
427
428                return false
429        }
430
431        /**
432         * Returns true if the given user is allowed to write this study
433         */
434        public boolean canWrite(SecUser loggedInUser) {
435                if (loggedInUser == null) {
436                        return false;
437                }
438
439                // Administrators are allowed to write every study
440                if (loggedInUser.hasAdminRights()) {
441                        return true;
442                }
443
444                return this.owner == loggedInUser || this.writers.contains(loggedInUser)
445        }
446
447        /**
448         * Returns true if the given user is the owner of this study
449         */
450        public boolean isOwner(SecUser loggedInUser) {
451                if (loggedInUser == null) {
452                        return false;
453                }
454                return this.owner == loggedInUser
455        }
456
457        /**
458         * Returns a list of studies that are writable for the given user
459         */
460        public static giveWritableStudies(SecUser user, Integer max = null) {
461                // User that are not logged in, are not allowed to write to a study
462                if (user == null)
463                        return [];
464
465                def c = Study.createCriteria()
466
467                // Administrators are allowed to read everything
468                if (user.hasAdminRights()) {
469                        return c.listDistinct {
470                                if (max != null) maxResults(max)
471                                order("title", "asc")
472                               
473                        }
474                }
475
476                return c.listDistinct {
477                        if (max != null) maxResults(max)
478                        order("title", "asc")
479                        or {
480                                eq("owner", user)
481                                writers {
482                                        eq("id", user.id)
483                                }
484                        }
485                }
486        }
487
488        /**
489         * Returns a list of studies that are readable by the given user
490         */
491        public static giveReadableStudies(SecUser user, Integer max = null, int offset = 0) {
492                def c = Study.createCriteria()
493
494                // Administrators are allowed to read everything
495                if (user == null) {
496                        return c.listDistinct {
497                                if (max != null) maxResults(max)
498                                firstResult(offset)
499                                order("title", "asc")
500                                and {
501                                        eq("published", true)
502                                        eq("publicstudy", true)
503                                }
504                        }
505                } else if (user.hasAdminRights()) {
506                        return c.listDistinct {
507                                if (max != null) maxResults(max)
508                                firstResult(offset)
509                                order("title", "asc")
510                        }
511                } else {
512                        return c.listDistinct {
513                                if (max != null) maxResults(max)
514                                firstResult(offset)
515                                order("title", "asc")
516                                or {
517                                        eq("owner", user)
518                                        writers {
519                                                eq("id", user.id)
520                                        }
521                                        and {
522                                                readers {
523                                                        eq("id", user.id)
524                                                }
525                                                eq("published", true)
526                                        }
527                                }
528                        }
529                }
530        }
531
532        /**
533         * perform a text search on studies
534         * @param query
535         * @return
536         */
537        public static textSearchReadableStudies(SecUser user, String query) {
538                def c = Study.createCriteria()
539
540                if (user == null) {
541                        // regular user
542                        return c.listDistinct {
543                                or {
544                                        ilike("title", "%${query}%")
545                                        ilike("description", "%${query}%")
546                                }
547                                and {
548                                        eq("published", true)
549                                        eq("publicstudy", true)
550                                }
551                        }
552                } else if (user.hasAdminRights()) {
553                        // admin can search everything
554                        return c.listDistinct {
555                                or {
556                                        ilike("title", "%${query}%")
557                                        ilike("description", "%${query}%")
558                                }
559                        }
560                } else {
561                        return c.listDistinct {
562                                or {
563                                        ilike("title", "%${query}%")
564                                        ilike("description", "%${query}%")
565                                }
566                                and {
567                                        or {
568                                                eq("owner", user)
569                                                writers {
570                                                        eq("id", user.id)
571                                                }
572                                                and {
573                                                        readers {
574                                                                eq("id", user.id)
575                                                        }
576                                                        eq("published", true)
577                                                }
578                                        }
579                                }
580                        }
581
582                }
583        }
584
585        /**
586         * Returns the number of public studies
587         * @return int
588         */
589        public static countPublicStudies() { return countPublicStudies(true) }
590        public static countPublicStudies(boolean published) {
591                def c = Study.createCriteria()
592                return (c.listDistinct {
593                        and {
594                                eq("published", published)
595                                eq("publicstudy", true)
596                        }
597                }).size()
598        }
599
600        /**
601         * Returns the number of private studies
602         * @return int
603         */
604        public static countPrivateStudies() { return countPrivateStudies(false) }
605        public static countPrivateStudies(boolean published) {
606                def c = Study.createCriteria()
607                return (c.listDistinct {
608                        and {
609                                eq("publicstudy", false)
610                        }
611                        or {
612                                eq("published", published)
613                                eq("publicstudy", true)
614                        }
615                }).size()
616        }
617
618        /**
619         * Returns the number of studies that are readable by the given user
620         */
621        public static countReadableStudies(SecUser user) {
622                def c = Study.createCriteria()
623
624                // got a user?
625                if (user == null) {
626                        return c.count {
627                                and {
628                                        eq("published", true)
629                                        eq("publicstudy", true)
630                                }
631                        }
632                } else if (user.hasAdminRights()) {
633                        // Administrators are allowed to read everything
634                        return Study.count()
635                } else {
636                        return (c.listDistinct {
637                                or {
638                                        eq("owner", user)
639                                        writers {
640                                                eq("id", user.id)
641                                        }
642                                        and {
643                                                readers {
644                                                        eq("id", user.id)
645                                                }
646                                                eq("published", true)
647                                        }
648                                }
649                        }).size()
650                }
651        }
652
653        /**
654         * Returns the number of studies that are readable & writable by the given user
655         */
656        public static countReadableAndWritableStudies(SecUser user) {
657                def c = Study.createCriteria()
658
659                // got a user?
660                if (user == null) {
661                        return 0
662                } else if (user.hasAdminRights()) {
663                        return Study.count()
664                } else {
665                        return (c.listDistinct {
666                                or {
667                                        eq("owner", user)
668                                        writers {
669                                                eq("id", user.id)
670                                        }
671                                }
672                        }).size()
673                }
674        }
675
676        /**
677         * Returns the UUID of this study and generates one if needed
678         */
679        public String giveUUID() {
680                if( !this.studyUUID ) {
681                        this.studyUUID = UUID.randomUUID().toString();
682                        if( !this.save(flush:true) ) {
683                                log.error "Couldn't save study UUID: " + this.getErrors();
684                        }
685                }
686
687                return this.studyUUID;
688        }
689
690        /**
691         * Basic equals method to check whether objects are equals, by comparing the ids
692         * @param o             Object to compare with
693         * @return              True iff the id of the given Study is equal to the id of this Study
694         */
695        public boolean equals( Object o ) {
696                if( o == null )
697                        return false;
698
699                if( !( o instanceof Study ) )
700                        return false
701
702                Study s = (Study) o;
703
704                return this.id == s.id
705        }
706
707    // This closure is used in the before{Insert,Update,Delete} closures below.
708    // It is necessary to prevent flushing in the same session as a top level
709    // database action such as 'save' or 'addTo...'. This confuses hibernate and
710    // produces hard to trace errors.
711    // The same holds for flushing during validation (but that's not the case
712    // here).
713    // http://grails.1312388.n4.nabble.com/Grails-hibernate-flush-causes-IndexOutOfBoundsException-td3031979.html
714    static manualFlush(closure) {
715        withSession {session ->
716            def save
717            try {
718                save = session.flushMode
719                session.flushMode = org.hibernate.FlushMode.MANUAL
720                closure()
721            } finally {
722                if (save) {
723                    session.flushMode = save
724                }
725         }
726        }
727    }
728
729        // Send messages to modules about changes in this study
730        def beforeInsert = {
731        manualFlush{
732            moduleNotificationService.invalidateStudy( this )
733        }
734        }
735        def beforeUpdate = {
736        manualFlush{
737            moduleNotificationService.invalidateStudy( this )
738        }
739        }
740        def beforeDelete = {
741                manualFlush{
742            moduleNotificationService.invalidateStudy( this )
743        }
744        }
745    }
Note: See TracBrowser for help on using the browser.