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

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

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

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