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

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