source: trunk/grails-app/domain/dbnp/studycapturing/Study.groovy

Last change on this file was 2246, checked in by business@…, 7 years ago

added caching to Assay and Study to speed up a.o. REST calls from modules

  • Property svn:keywords set to Rev Author Date
File size: 20.3 KB
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: 2246 $
10 * $Author: work@osx.eu $
11 * $Date: 2012-06-05 08:25:12 +0000 (di, 05 jun 2012) $
12 */
13class Study extends TemplateEntity {
14        def moduleNotificationService
15
16        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
17        String title            // The title of the study
18        String description      // A brief synopsis of what the study is about
19        String code                     // currently used as the external study ID, e.g. to reference a study in a SAM module
20        Date dateCreated
21        Date lastUpdated
22        Date startDate
23        List subjects
24        List events
25        List samplingEvents
26        List eventGroups
27        List samples
28        List assays
29        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
30        boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
31
32        /**
33         * UUID of this study
34         */
35        String studyUUID
36
37
38        static hasMany = [
39                subjects: Subject,
40                samplingEvents: SamplingEvent,
41                events: Event,
42                eventGroups: EventGroup,
43                samples: Sample,
44                assays: Assay,
45                persons: StudyPerson,
46                publications: Publication,
47                readers: SecUser,
48                writers: SecUser
49        ]
50
51        static constraints = {
52                title(nullable:false, blank: false, unique:true, maxSize: 255)
53                owner(nullable: true, blank: true)
54                code(nullable: true, blank: true, unique: true, maxSize: 255)
55                studyUUID(nullable:true, unique:true, maxSize: 255)
56                persons(size:1..1000)
57                // TODO: add custom validator for 'published' to assess whether the study meets all quality criteria for publication
58                // tested by SampleTests.testStudyPublish
59        }
60
61        // see org.dbnp.gdt.FuzzyStringMatchController and Service
62        static fuzzyStringMatchable = [
63            "title",
64                "code"
65        ]
66
67        static mapping = {
68                cache true
69                autoTimestamp true
70                sort "title"
71
72                // Make sure the TEXT field description is persisted with a TEXT field in the database
73                description type: 'text'
74                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
75                templateTextFields type: 'text'
76
77        }
78
79        // The external identifier (studyToken) is currently the code of the study.
80        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
81
82        def getToken() { return giveUUID() }
83
84        /**
85         * return the domain fields for this domain class
86         * @return List
87         */
88        static List<TemplateField> giveDomainFields() { return Study.domainFields }
89
90        static final List<TemplateField> domainFields = [
91                new TemplateField(
92                name: 'title',
93                type: TemplateFieldType.STRING,
94                required: true),
95                new TemplateField(
96                name: 'description',
97                type: TemplateFieldType.TEXT,
98                comment:'Give a brief synopsis of what your study is about',
99                required: true),
100                new TemplateField(
101                name: 'code',
102                type: TemplateFieldType.STRING,
103                preferredIdentifier: true,
104                comment: 'Fill out the code by which many people will recognize your study',
105                required: false),
106                new TemplateField(
107                name: 'startDate',
108                type: TemplateFieldType.DATE,
109                comment: 'Fill out the official start date or date of first action',
110                required: true),
111                new TemplateField(
112                name: 'published',
113                type: TemplateFieldType.BOOLEAN,
114                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.',
115                required: false)
116        ]
117
118        /**
119         * return the title of this study
120         */
121        def String toString() {
122                return ( (code) ? code : "[no code]") + " - "+ title
123        }
124
125        /**
126         * returns all events and sampling events that do not belong to a group
127         */
128        def List<Event> getOrphanEvents() {
129                def orphans = events.findAll { event -> !event.belongsToGroup(eventGroups) } +
130                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
131
132                return orphans
133        }
134
135        /**
136         * Return the unique Subject templates that are used in this study
137         */
138        def List<Template> giveSubjectTemplates() {
139                TemplateEntity.giveTemplates(subjects)
140        }
141
142        /**
143         * Return all subjects for a specific template
144         * @param Template
145         * @return ArrayList
146         */
147        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
148                subjects.findAll { it.template.equals(template) }
149        }
150
151        /**
152         * Return all unique assay templates
153         * @return Set
154         */
155        List<Template> giveAllAssayTemplates() {
156                TemplateEntity.giveTemplates(((assays) ? assays : []))
157        }
158
159        /**
160         * Return all assays for a particular template
161         * @return ArrayList
162         */
163        def ArrayList giveAssaysForTemplate(Template template) {
164                assays.findAll { it && it.template.equals(template) }
165        }
166
167        /**
168         * Return the unique Event and SamplingEvent templates that are used in this study
169         */
170        List<Template> giveAllEventTemplates() {
171                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
172                // gives trouble when asking .size() to the result
173                // So we also use giveTemplates here
174                TemplateEntity.giveTemplates(((events) ? events : []) + ((samplingEvents) ? samplingEvents : []))
175        }
176
177        /**
178         * Return all events and samplingEvenets for a specific template
179         * @param Template
180         * @return ArrayList
181         */
182        def ArrayList giveEventsForTemplate(Template template) {
183                def events = events.findAll { it.template.equals(template) }
184                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
185
186                return (events) ? events : samplingEvents
187        }
188
189        /**
190         * Return the unique Event templates that are used in this study
191         */
192        List<Template> giveEventTemplates() {
193                TemplateEntity.giveTemplates(events)
194        }
195
196        /**
197         * Return the unique SamplingEvent templates that are used in this study
198         */
199        List<Template> giveSamplingEventTemplates() {
200                TemplateEntity.giveTemplates(samplingEvents)
201        }
202
203        /**
204         * Returns the unique Sample templates that are used in the study
205         */
206        List<Template> giveSampleTemplates() {
207                TemplateEntity.giveTemplates(samples)
208        }
209
210        /**
211         * Return all samples for a specific template, sorted by subject name
212         * @param Template
213         * @return ArrayList
214         */
215        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
216                // sort in a concatenated string as sorting on 3 seperate elements
217                // in a map does not seem to work properly
218                samples.findAll { it.template.equals(template) }.sort {
219                        "${it.parentEvent?.template}|${it.parentEvent?.startTime}|${it.parentSubject?.name}".toLowerCase()
220                }
221        }
222
223        /**
224         * Returns the template of the study
225         */
226        Template giveStudyTemplate() {
227                return this.template
228        }
229
230        /**
231         * Delete a specific subject from this study, including all its relations
232         * @param subject The subject to be deleted
233         * @void
234         */
235        void deleteSubject(Subject subject) {
236                // Delete the subject from the event groups it was referenced in
237                this.eventGroups.each {
238                        if (it.subjects?.contains(subject)) {
239                                it.removeFromSubjects(subject)
240                        }
241                }
242
243                // Delete the samples that have this subject as parent
244                this.samples.findAll { it.parentSubject.equals(subject) }.each {
245                        this.deleteSample(it)
246                }
247
248                // This should remove the subject itself too, because of the cascading belongsTo relation
249                this.removeFromSubjects(subject)
250
251                // But apparently it needs an explicit delete() too
252                subject.delete()
253        }
254
255        /**
256         * Delete an assay from the study
257         * @param Assay
258         * @void
259         */
260        def deleteAssay(Assay assay) {
261                if (assay && assay instanceof Assay) {
262                        // iterate through linked samples
263                        assay.samples.findAll { true }.each() { sample ->
264                                assay.removeFromSamples(sample)
265                        }
266
267                        // remove this assay from the study
268                        this.removeFromAssays(assay)
269
270                        // and delete it explicitly
271                        assay.delete()
272                }
273        }
274
275        /**
276         * Delete an event from the study, including all its relations
277         * @param Event
278         * @void
279         */
280        void deleteEvent(Event event) {
281                // remove event from eventGroups
282                this.eventGroups.each() { eventGroup ->
283                        eventGroup.removeFromEvents(event)
284                }
285
286                // remove event from the study
287                this.removeFromEvents(event)
288
289                // and perform a hard delete
290                event.delete()
291        }
292
293        /**
294         * Delete a sample from the study, including all its relations
295         * @param Event
296         * @void
297         */
298        void deleteSample(Sample sample) {
299                // remove the sample from the study
300                this.removeFromSamples(sample)
301
302                // remove the sample from any sampling events it belongs to
303                this.samplingEvents.findAll { it.samples.any { it == sample }}.each {
304                        it.removeFromSamples(sample)
305                }
306
307                // remove the sample from any assays it belongs to
308                this.assays.findAll { it.samples.any { it == sample }}.each {
309                        it.removeFromSamples(sample)
310                }
311
312                // Also here, contrary to documentation, an extra delete() is needed
313                // otherwise date is not properly deleted!
314                sample.delete()
315        }
316
317        /**
318         * Delete a samplingEvent from the study, including all its relations
319         * @param SamplingEvent
320         * @void
321         */
322        void deleteSamplingEvent(SamplingEvent samplingEvent) {
323                // remove event from eventGroups
324                this.eventGroups.each() { eventGroup ->
325                        eventGroup.removeFromSamplingEvents(samplingEvent)
326                }
327
328                // Delete the samples that have this sampling event as parent
329                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
330                        // This should remove the sample itself too, because of the cascading belongsTo relation
331                        this.deleteSample(it)
332                }
333
334                // Remove event from the study
335                // This should remove the event group itself too, because of the cascading belongsTo relation
336                this.removeFromSamplingEvents(samplingEvent)
337
338                // But apparently it needs an explicit delete() too
339                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
340                samplingEvent.delete()
341        }
342
343        /**
344         * Delete an eventGroup from the study, including all its relations
345         * @param EventGroup
346         * @void
347         */
348        void deleteEventGroup(EventGroup eventGroup) {
349                // If the event group contains sampling events
350                if (eventGroup.samplingEvents) {
351                        // remove all samples that originate from this eventGroup
352                        if (eventGroup.samplingEvents.size()) {
353                                // find all samples that
354                                //      - are part of this study
355                                this.samples.findAll { sample ->
356                                        (
357                                                // - belong to this eventGroup
358                                                (
359                                                        sample.parentEventGroup &&
360                                                        (
361                                                                (sample.parentEventGroup.id && eventGroup.id && sample.parentEventGroup.id == eventGroup.id)
362                                                                ||
363                                                                (sample.parentEventGroup.getIdentifier() == eventGroup.getIdentifier())
364                                                                ||
365                                                                sample.parentEventGroup.equals(eventGroup)
366                                                        )
367                                                )
368                                        )
369                                }
370                                .each() { sample ->
371                                        // remove sample from study
372                                        this.deleteSample(sample)
373                                }
374                        }
375
376                        // remove all samplingEvents from this eventGroup
377                        eventGroup.samplingEvents.findAll {}.each() {
378                                eventGroup.removeFromSamplingEvents(it)
379                        }
380                }
381
382                // If the event group contains subjects
383                if (eventGroup.subjects) {
384                        // remove all subject from this eventGroup
385                        eventGroup.subjects.findAll {}.each() {
386                                eventGroup.removeFromSubjects(it)
387                        }
388                }
389
390                // remove the eventGroup from the study
391                this.removeFromEventGroups(eventGroup)
392
393                // Also here, contrary to documentation, an extra delete() is needed
394                // otherwise cascaded deletes are not properly performed
395                eventGroup.delete()
396        }
397
398        /**
399         * Returns true if the given user is allowed to read this study
400         */
401        public boolean canRead(SecUser loggedInUser) {
402                // Public studies may be read by anyone
403                if( this.publicstudy && this.published ) {
404                        return true;
405                }
406               
407                // Anonymous readers are only given access when published and public
408                if (loggedInUser == null) {
409                        return false;
410                }
411
412                // Administrators are allowed to read every study
413                if (loggedInUser.hasAdminRights()) {
414                        return true;
415                }
416
417                // Owners and writers are allowed to read this study
418                if (this.owner == loggedInUser || this.writers.contains(loggedInUser)) {
419                        return true
420                }
421
422                // Readers are allowed to read this study when it is published
423//              if (this.readers.contains(loggedInUser) && this.published) {
424                if (this.readers.contains(loggedInUser)) {
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    /**
708     * Returns the minimum and maximum date of the events of this study
709     * @return  A map containing absolute minDate and maxDate (not relative)
710     */
711    def getMinMaxEventDate() {
712        long minDate = Long.MAX_VALUE;
713        long maxDate = Long.MIN_VALUE;
714        this.events.each {
715            if(it.startTime < minDate) {
716                minDate = it.startTime;
717            }
718            if(it.endTime > maxDate) {
719                maxDate = it.endTime;
720            }
721            if(it.startTime > maxDate) {
722                maxDate = it.startTime;
723            }
724        }
725        this.samplingEvents.each {
726            if(it.startTime < minDate) {
727                minDate = it.startTime;
728            }
729            if(it.startTime > maxDate) {
730                maxDate = it.startTime;
731            }
732        }
733        long lngStartDate  = (Long) this.startDate.getTime();
734        return ["minDate" : new Date( lngStartDate + minDate * 1000 ), "maxDate" : new Date( lngStartDate + maxDate * 1000 )];
735    }
736
737    // This closure is used in the before{Insert,Update,Delete} closures below.
738    // It is necessary to prevent flushing in the same session as a top level
739    // database action such as 'save' or 'addTo...'. This confuses hibernate and
740    // produces hard to trace errors.
741    // The same holds for flushing during validation (but that's not the case
742    // here).
743    // http://grails.1312388.n4.nabble.com/Grails-hibernate-flush-causes-IndexOutOfBoundsException-td3031979.html
744    static manualFlush(closure) {
745        withSession {session ->
746            def save
747            try {
748                save = session.flushMode
749                session.flushMode = org.hibernate.FlushMode.MANUAL
750                closure()
751            } finally {
752                if (save) {
753                    session.flushMode = save
754                }
755         }
756        }
757    }
758
759        // Send messages to modules about changes in this study
760        def beforeInsert = {
761        manualFlush{
762            moduleNotificationService.invalidateStudy( this )
763        }
764        }
765        def beforeUpdate = {
766        manualFlush{
767            moduleNotificationService.invalidateStudy( this )
768        }
769        }
770        def beforeDelete = {
771                manualFlush{
772            moduleNotificationService.invalidateStudy( this )
773        }
774        }
775
776    /**
777     * return the unique species
778     * @see dbnp.query.StudyCompareController
779     */
780    def uniqueSpecies = {
781        return subjects.collect{ it.species }.unique()
782    }
783
784    /**
785     * return the unique event templates
786     * @see dbnp.query.StudyCompareController
787     */
788    def uniqueEventTemplates = {
789        return events.collect{ it.template }.unique()
790    }
791
792    /**
793     * return the unique sampling event templates
794     * @see dbnp.query.StudyCompareController
795     */
796    def uniqueSamplingEventTemplates = {
797        return samplingEvents.collect{ it.template }.unique()
798    }
799
800    /**
801     * return the unique assay modules
802     * @see dbnp.query.StudyCompareController
803     */
804    def uniqueAssayModules = {
805        return assays.collect{ it.module }.unique()
806    }
807}
Note: See TracBrowser for help on using the repository browser.