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

Revision 2219, 19.5 KB (checked in by work@…, 2 years ago)

removed the 'published' limitation from canRead

  • 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        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                autoTimestamp true
69                sort "title"
70
71                // Make sure the TEXT field description is persisted with a TEXT field in the database
72                description type: 'text'
73                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
74                templateTextFields type: 'text'
75
76        }
77
78        // The external identifier (studyToken) is currently the code of the study.
79        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
80
81        def getToken() { return giveUUID() }
82
83        /**
84         * return the domain fields for this domain class
85         * @return List
86         */
87        static List<TemplateField> giveDomainFields() { return Study.domainFields }
88
89        static final List<TemplateField> domainFields = [
90                new TemplateField(
91                name: 'title',
92                type: TemplateFieldType.STRING,
93                required: true),
94                new TemplateField(
95                name: 'description',
96                type: TemplateFieldType.TEXT,
97                comment:'Give a brief synopsis of what your study is about',
98                required: true),
99                new TemplateField(
100                name: 'code',
101                type: TemplateFieldType.STRING,
102                preferredIdentifier: true,
103                comment: 'Fill out the code by which many people will recognize your study',
104                required: false),
105                new TemplateField(
106                name: 'startDate',
107                type: TemplateFieldType.DATE,
108                comment: 'Fill out the official start date or date of first action',
109                required: true),
110                new TemplateField(
111                name: 'published',
112                type: TemplateFieldType.BOOLEAN,
113                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.',
114                required: false)
115        ]
116
117        /**
118         * return the title of this study
119         */
120        def String toString() {
121                return ( (code) ? code : "[no code]") + " - "+ title
122        }
123
124        /**
125         * returns all events and sampling events that do not belong to a group
126         */
127        def List<Event> getOrphanEvents() {
128                def orphans = events.findAll { event -> !event.belongsToGroup(eventGroups) } +
129                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
130
131                return orphans
132        }
133
134        /**
135         * Return the unique Subject templates that are used in this study
136         */
137        def List<Template> giveSubjectTemplates() {
138                TemplateEntity.giveTemplates(subjects)
139        }
140
141        /**
142         * Return all subjects for a specific template
143         * @param Template
144         * @return ArrayList
145         */
146        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
147                subjects.findAll { it.template.equals(template) }
148        }
149
150        /**
151         * Return all unique assay templates
152         * @return Set
153         */
154        List<Template> giveAllAssayTemplates() {
155                TemplateEntity.giveTemplates(((assays) ? assays : []))
156        }
157
158        /**
159         * Return all assays for a particular template
160         * @return ArrayList
161         */
162        def ArrayList giveAssaysForTemplate(Template template) {
163                assays.findAll { it && it.template.equals(template) }
164        }
165
166        /**
167         * Return the unique Event and SamplingEvent templates that are used in this study
168         */
169        List<Template> giveAllEventTemplates() {
170                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
171                // gives trouble when asking .size() to the result
172                // So we also use giveTemplates here
173                TemplateEntity.giveTemplates(((events) ? events : []) + ((samplingEvents) ? samplingEvents : []))
174        }
175
176        /**
177         * Return all events and samplingEvenets for a specific template
178         * @param Template
179         * @return ArrayList
180         */
181        def ArrayList giveEventsForTemplate(Template template) {
182                def events = events.findAll { it.template.equals(template) }
183                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
184
185                return (events) ? events : samplingEvents
186        }
187
188        /**
189         * Return the unique Event templates that are used in this study
190         */
191        List<Template> giveEventTemplates() {
192                TemplateEntity.giveTemplates(events)
193        }
194
195        /**
196         * Return the unique SamplingEvent templates that are used in this study
197         */
198        List<Template> giveSamplingEventTemplates() {
199                TemplateEntity.giveTemplates(samplingEvents)
200        }
201
202        /**
203         * Returns the unique Sample templates that are used in the study
204         */
205        List<Template> giveSampleTemplates() {
206                TemplateEntity.giveTemplates(samples)
207        }
208
209        /**
210         * Return all samples for a specific template, sorted by subject name
211         * @param Template
212         * @return ArrayList
213         */
214        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
215                // sort in a concatenated string as sorting on 3 seperate elements
216                // in a map does not seem to work properly
217                samples.findAll { it.template.equals(template) }.sort {
218                        "${it.parentEvent?.template}|${it.parentEvent?.startTime}|${it.parentSubject?.name}".toLowerCase()
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 that
353                                //      - are part of this study
354                                this.samples.findAll { sample ->
355                                        (
356                                                // - belong to this eventGroup
357                                                (
358                                                        sample.parentEventGroup &&
359                                                        (
360                                                                (sample.parentEventGroup.id && eventGroup.id && sample.parentEventGroup.id == eventGroup.id)
361                                                                ||
362                                                                (sample.parentEventGroup.getIdentifier() == eventGroup.getIdentifier())
363                                                                ||
364                                                                sample.parentEventGroup.equals(eventGroup)
365                                                        )
366                                                )
367                                        )
368                                }
369                                .each() { sample ->
370                                        // remove sample from study
371                                        this.deleteSample(sample)
372                                }
373                        }
374
375                        // remove all samplingEvents from this eventGroup
376                        eventGroup.samplingEvents.findAll {}.each() {
377                                eventGroup.removeFromSamplingEvents(it)
378                        }
379                }
380
381                // If the event group contains subjects
382                if (eventGroup.subjects) {
383                        // remove all subject from this eventGroup
384                        eventGroup.subjects.findAll {}.each() {
385                                eventGroup.removeFromSubjects(it)
386                        }
387                }
388
389                // remove the eventGroup from the study
390                this.removeFromEventGroups(eventGroup)
391
392                // Also here, contrary to documentation, an extra delete() is needed
393                // otherwise cascaded deletes are not properly performed
394                eventGroup.delete()
395        }
396
397        /**
398         * Returns true if the given user is allowed to read this study
399         */
400        public boolean canRead(SecUser loggedInUser) {
401                // Public studies may be read by anyone
402                if( this.publicstudy && this.published ) {
403                        return true;
404                }
405               
406                // Anonymous readers are only given access when published and public
407                if (loggedInUser == null) {
408                        return false;
409                }
410
411                // Administrators are allowed to read every study
412                if (loggedInUser.hasAdminRights()) {
413                        return true;
414                }
415
416                // Owners and writers are allowed to read this study
417                if (this.owner == loggedInUser || this.writers.contains(loggedInUser)) {
418                        return true
419                }
420
421                // Readers are allowed to read this study when it is published
422//              if (this.readers.contains(loggedInUser) && this.published) {
423                if (this.readers.contains(loggedInUser)) {
424                        return true
425                }
426
427                return false
428        }
429
430        /**
431         * Returns true if the given user is allowed to write this study
432         */
433        public boolean canWrite(SecUser loggedInUser) {
434                if (loggedInUser == null) {
435                        return false;
436                }
437
438                // Administrators are allowed to write every study
439                if (loggedInUser.hasAdminRights()) {
440                        return true;
441                }
442
443                return this.owner == loggedInUser || this.writers.contains(loggedInUser)
444        }
445
446        /**
447         * Returns true if the given user is the owner of this study
448         */
449        public boolean isOwner(SecUser loggedInUser) {
450                if (loggedInUser == null) {
451                        return false;
452                }
453                return this.owner == loggedInUser
454        }
455
456        /**
457         * Returns a list of studies that are writable for the given user
458         */
459        public static giveWritableStudies(SecUser user, Integer max = null) {
460                // User that are not logged in, are not allowed to write to a study
461                if (user == null)
462                        return [];
463
464                def c = Study.createCriteria()
465
466                // Administrators are allowed to read everything
467                if (user.hasAdminRights()) {
468                        return c.listDistinct {
469                                if (max != null) maxResults(max)
470                                order("title", "asc")
471                               
472                        }
473                }
474
475                return c.listDistinct {
476                        if (max != null) maxResults(max)
477                        order("title", "asc")
478                        or {
479                                eq("owner", user)
480                                writers {
481                                        eq("id", user.id)
482                                }
483                        }
484                }
485        }
486
487        /**
488         * Returns a list of studies that are readable by the given user
489         */
490        public static giveReadableStudies(SecUser user, Integer max = null, int offset = 0) {
491                def c = Study.createCriteria()
492
493                // Administrators are allowed to read everything
494                if (user == null) {
495                        return c.listDistinct {
496                                if (max != null) maxResults(max)
497                                firstResult(offset)
498                                order("title", "asc")
499                                and {
500//                                      eq("published", true)
501                                        eq("publicstudy", true)
502                                }
503                        }
504                } else if (user.hasAdminRights()) {
505                        return c.listDistinct {
506                                if (max != null) maxResults(max)
507                                firstResult(offset)
508                                order("title", "asc")
509                        }
510                } else {
511                        return c.listDistinct {
512                                if (max != null) maxResults(max)
513                                firstResult(offset)
514                                order("title", "asc")
515                                or {
516                                        eq("owner", user)
517                                        writers {
518                                                eq("id", user.id)
519                                        }
520                                        and {
521                                                readers {
522                                                        eq("id", user.id)
523                                                }
524//                                              eq("published", true)
525                                        }
526                                }
527                        }
528                }
529        }
530
531        /**
532         * perform a text search on studies
533         * @param query
534         * @return
535         */
536        public static textSearchReadableStudies(SecUser user, String query) {
537                def c = Study.createCriteria()
538
539                if (user == null) {
540                        // regular user
541                        return c.listDistinct {
542                                or {
543                                        ilike("title", "%${query}%")
544                                        ilike("description", "%${query}%")
545                                }
546                                and {
547                                        eq("published", true)
548                                        eq("publicstudy", true)
549                                }
550                        }
551                } else if (user.hasAdminRights()) {
552                        // admin can search everything
553                        return c.listDistinct {
554                                or {
555                                        ilike("title", "%${query}%")
556                                        ilike("description", "%${query}%")
557                                }
558                        }
559                } else {
560                        return c.listDistinct {
561                                or {
562                                        ilike("title", "%${query}%")
563                                        ilike("description", "%${query}%")
564                                }
565                                and {
566                                        or {
567                                                eq("owner", user)
568                                                writers {
569                                                        eq("id", user.id)
570                                                }
571                                                and {
572                                                        readers {
573                                                                eq("id", user.id)
574                                                        }
575                                                        eq("published", true)
576                                                }
577                                        }
578                                }
579                        }
580
581                }
582        }
583
584        /**
585         * Returns the number of public studies
586         * @return int
587         */
588        public static countPublicStudies() { return countPublicStudies(true) }
589        public static countPublicStudies(boolean published) {
590                def c = Study.createCriteria()
591                return (c.listDistinct {
592                        and {
593                                eq("published", published)
594                                eq("publicstudy", true)
595                        }
596                }).size()
597        }
598
599        /**
600         * Returns the number of private studies
601         * @return int
602         */
603        public static countPrivateStudies() { return countPrivateStudies(false) }
604        public static countPrivateStudies(boolean published) {
605                def c = Study.createCriteria()
606                return (c.listDistinct {
607                        and {
608                                eq("publicstudy", false)
609                        }
610                        or {
611                                eq("published", published)
612                                eq("publicstudy", true)
613                        }
614                }).size()
615        }
616
617        /**
618         * Returns the number of studies that are readable by the given user
619         */
620        public static countReadableStudies(SecUser user) {
621                def c = Study.createCriteria()
622
623                // got a user?
624                if (user == null) {
625                        return c.count {
626                                and {
627                                        eq("published", true)
628                                        eq("publicstudy", true)
629                                }
630                        }
631                } else if (user.hasAdminRights()) {
632                        // Administrators are allowed to read everything
633                        return Study.count()
634                } else {
635                        return (c.listDistinct {
636                                or {
637                                        eq("owner", user)
638                                        writers {
639                                                eq("id", user.id)
640                                        }
641                                        and {
642                                                readers {
643                                                        eq("id", user.id)
644                                                }
645                                                eq("published", true)
646                                        }
647                                }
648                        }).size()
649                }
650        }
651
652        /**
653         * Returns the number of studies that are readable & writable by the given user
654         */
655        public static countReadableAndWritableStudies(SecUser user) {
656                def c = Study.createCriteria()
657
658                // got a user?
659                if (user == null) {
660                        return 0
661                } else if (user.hasAdminRights()) {
662                        return Study.count()
663                } else {
664                        return (c.listDistinct {
665                                or {
666                                        eq("owner", user)
667                                        writers {
668                                                eq("id", user.id)
669                                        }
670                                }
671                        }).size()
672                }
673        }
674
675        /**
676         * Returns the UUID of this study and generates one if needed
677         */
678        public String giveUUID() {
679                if( !this.studyUUID ) {
680                        this.studyUUID = UUID.randomUUID().toString();
681                        if( !this.save(flush:true) ) {
682                                log.error "Couldn't save study UUID: " + this.getErrors();
683                        }
684                }
685
686                return this.studyUUID;
687        }
688
689        /**
690         * Basic equals method to check whether objects are equals, by comparing the ids
691         * @param o             Object to compare with
692         * @return              True iff the id of the given Study is equal to the id of this Study
693         */
694        public boolean equals( Object o ) {
695                if( o == null )
696                        return false;
697
698                if( !( o instanceof Study ) )
699                        return false
700
701                Study s = (Study) o;
702
703                return this.id == s.id
704        }
705
706    /**
707     * Returns the minimum and maximum date of the events of this study
708     * @return  A map containing absolute minDate and maxDate (not relative)
709     */
710    def getMinMaxEventDate() {
711        long minDate = Long.MAX_VALUE;
712        long maxDate = Long.MIN_VALUE;
713        this.events.each {
714            if(it.startTime < minDate) {
715                minDate = it.startTime;
716            }
717            if(it.endTime > maxDate) {
718                maxDate = it.endTime;
719            }
720            if(it.startTime > maxDate) {
721                maxDate = it.startTime;
722            }
723        }
724        this.samplingEvents.each {
725            if(it.startTime < minDate) {
726                minDate = it.startTime;
727            }
728            if(it.startTime > maxDate) {
729                maxDate = it.startTime;
730            }
731        }
732        long lngStartDate  = (Long) this.startDate.getTime();
733        return ["minDate" : new Date( lngStartDate + minDate * 1000 ), "maxDate" : new Date( lngStartDate + maxDate * 1000 )];
734    }
735
736    // This closure is used in the before{Insert,Update,Delete} closures below.
737    // It is necessary to prevent flushing in the same session as a top level
738    // database action such as 'save' or 'addTo...'. This confuses hibernate and
739    // produces hard to trace errors.
740    // The same holds for flushing during validation (but that's not the case
741    // here).
742    // http://grails.1312388.n4.nabble.com/Grails-hibernate-flush-causes-IndexOutOfBoundsException-td3031979.html
743    static manualFlush(closure) {
744        withSession {session ->
745            def save
746            try {
747                save = session.flushMode
748                session.flushMode = org.hibernate.FlushMode.MANUAL
749                closure()
750            } finally {
751                if (save) {
752                    session.flushMode = save
753                }
754         }
755        }
756    }
757
758        // Send messages to modules about changes in this study
759        def beforeInsert = {
760        manualFlush{
761            moduleNotificationService.invalidateStudy( this )
762        }
763        }
764        def beforeUpdate = {
765        manualFlush{
766            moduleNotificationService.invalidateStudy( this )
767        }
768        }
769        def beforeDelete = {
770                manualFlush{
771            moduleNotificationService.invalidateStudy( this )
772        }
773        }
774}
Note: See TracBrowser for help on using the browser.