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

Last change on this file since 1585 was 1585, checked in by work@…, 11 years ago

moved text search method into Study domain

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