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

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