• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CenterForOpenScience / ember-osf-web / 6828904474

10 Nov 2023 07:32PM UTC coverage: 70.49% (-0.2%) from 70.651%
6828904474

push

github

adlius
Merge tag '23.14.0' into develop

BOA addon

2598 of 3915 branches covered (0.0%)

Branch coverage included in aggregate %.

5968 of 8237 relevant lines covered (72.45%)

235.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.18
/app/models/node.ts
1
import { attr, belongsTo, hasMany, AsyncBelongsTo, AsyncHasMany } from '@ember-data/model';
2

3
import { computed } from '@ember/object';
4
import { alias, bool, equal, not } from '@ember/object/computed';
5
import { inject as service } from '@ember/service';
6
import { htmlSafe } from '@ember/template';
7
import { tracked } from '@glimmer/tracking';
8
import { buildValidations, validator } from 'ember-cp-validations';
9
import Intl from 'ember-intl/services/intl';
10

11
import config from 'ember-osf-web/config/environment';
12
import { task } from 'ember-concurrency';
13
import { waitFor } from '@ember/test-waiters';
14

15
import getRelatedHref from 'ember-osf-web/utils/get-related-href';
16

17
import AbstractNodeModel from 'ember-osf-web/models/abstract-node';
18
import CitationModel from './citation';
19
import CommentModel from './comment';
20
import ContributorModel from './contributor';
21
import IdentifierModel from './identifier';
22
import InstitutionModel from './institution';
23
import LicenseModel from './license';
24
import LogModel from './log';
25
import NodeStorageModel from './node-storage';
26
import { Permission } from './osf-model';
27
import PreprintModel from './preprint';
28
import RegionModel from './region';
29
import RegistrationModel from './registration';
30
import SubjectModel from './subject';
31
import WikiModel from './wiki';
32

33
const {
34
    OSF: {
35
        apiUrl,
36
        apiNamespace,
37
    },
38
} = config;
1✔
39

40
const Validations = buildValidations({
1✔
41
    title: [
42
        validator('presence', true),
43
    ],
44
});
45

46
const CollectableValidations = buildValidations({
1✔
47
    description: [
48
        validator('presence', {
49
            presence: true,
50
        }),
51
    ],
52
    license: [
53
        validator('presence', {
54
            presence: true,
55
        }),
56
    ],
57
    nodeLicense: [
58
        validator('presence', {
59
            presence: true,
60
        }),
61
        validator('node-license', {
62
            on: 'license',
63
        }),
64
    ],
65
    tags: [
66
        validator('presence', {
67
            presence: true,
68
            disabled: true,
69
        }),
70
    ],
71
}, {
72
    disabled: not('model.collectable'),
73
});
74

75
export enum NodeType {
76
    Fork = 'fork',
77
    Generic = 'generic',
78
    Registration = 'registration',
79
}
80

81
export enum NodeCategory {
82
    Data = 'data',
83
    Other = 'other',
84
    Project = 'project',
85
    Software = 'software',
86
    Analysis = 'analysis',
87
    Procedure = 'procedure',
88
    Hypothesis = 'hypothesis',
89
    Uncategorized = 'uncategorized',
90
    Communication = 'communication',
91
    Instrumentation = 'instrumentation',
92
    MethodsAndMeasures = 'methods and measures',
93
}
94

95
export interface NodeLicense {
96
    readonly copyrightHolders?: string;
97
    readonly year?: string;
98
}
99

100
export default class NodeModel extends AbstractNodeModel.extend(Validations, CollectableValidations) {
101
    @service intl!: Intl;
102

103
    @attr('fixstring') title!: string;
104
    @attr('fixstring') description!: string;
105
    @attr('node-category') category!: NodeCategory;
106
    @attr('boolean') currentUserIsContributor!: boolean;
107
    @attr('boolean') fork!: boolean;
108
    @alias('fork') isFork!: boolean;
109
    @attr('boolean') collection!: boolean;
110
    @attr('boolean') registration!: boolean;
111
    @attr('boolean') public!: boolean;
112
    @attr('date') dateCreated!: Date;
113
    @attr('date') dateModified!: Date;
114
    @attr('date') forkedDate!: Date;
115
    @attr('node-license') nodeLicense!: NodeLicense | null;
116
    @attr('fixstringarray') tags!: string[];
117
    @attr('fixstring') templateFrom!: string;
118
    @attr('string') analyticsKey?: string;
119
    @attr('boolean') preprint!: boolean;
120
    @attr('boolean') currentUserCanComment!: boolean;
121
    @attr('boolean') wikiEnabled!: boolean;
122

123
    // FE-only property to check enabled addons.
124
    // null until getEnabledAddons has been called
125
    @tracked addonsEnabled?: string[];
126

127
    @hasMany('contributor', { inverse: 'node' })
128
    contributors!: AsyncHasMany<ContributorModel> & ContributorModel[];
129

130
    @hasMany('contributor', { inverse: null })
131
    bibliographicContributors!: AsyncHasMany<ContributorModel>;
132

133
    @belongsTo('node', { inverse: 'children' })
134
    parent!: AsyncBelongsTo<NodeModel> & NodeModel;
135

136
    @belongsTo('region')
137
    region!: RegionModel;
138

139
    @hasMany('node', { inverse: 'parent' })
140
    children!: AsyncHasMany<NodeModel>;
141

142
    @hasMany('preprint', { inverse: 'node' })
143
    preprints!: AsyncHasMany<PreprintModel>;
144

145
    @hasMany('institution', { inverse: 'nodes' })
146
    affiliatedInstitutions!: AsyncHasMany<InstitutionModel> | InstitutionModel[];
147

148
    @hasMany('comment', { inverse: 'node' })
149
    comments!: AsyncHasMany<CommentModel>;
150

151
    @belongsTo('citation')
152
    citation!: AsyncBelongsTo<CitationModel> & CitationModel;
153

154
    @belongsTo('license', { inverse: null })
155
    license!: AsyncBelongsTo<LicenseModel> & LicenseModel;
156

157
    @hasMany('node', { inverse: null })
158
    linkedNodes!: AsyncHasMany<NodeModel> & NodeModel[];
159

160
    @hasMany('registration', { inverse: null })
161
    linkedRegistrations!: AsyncHasMany<RegistrationModel>;
162

163
    @hasMany('registration', { inverse: 'registeredFrom' })
164
    registrations!: AsyncHasMany<RegistrationModel>;
165

166
    @hasMany('node', { inverse: 'forkedFrom', polymorphic: true })
167
    forks!: AsyncHasMany<NodeModel>;
168

169
    @belongsTo('node', { inverse: 'forks', polymorphic: true })
170
    forkedFrom!: (AsyncBelongsTo<NodeModel> & NodeModel) | (AsyncBelongsTo<RegistrationModel> & RegistrationModel);
171

172
    @belongsTo('node', { inverse: null })
173
    root!: AsyncBelongsTo<NodeModel> & NodeModel;
174

175
    @belongsTo('node-storage', { inverse: null })
176
    storage!: AsyncBelongsTo<NodeStorageModel> & NodeStorageModel;
177

178
    @hasMany('node', { inverse: null })
179
    linkedByNodes!: AsyncHasMany<NodeModel>;
180

181
    @hasMany('node', { inverse: null })
182
    linkedByRegistrations!: AsyncHasMany<RegistrationModel>;
183

184
    @hasMany('wiki', { inverse: 'node' })
185
    wikis!: AsyncHasMany<WikiModel>;
186

187
    @hasMany('log', { inverse: 'originalNode' })
188
    logs!: AsyncHasMany<LogModel>;
189

190
    @hasMany('identifier', { inverse: 'referent' })
191
    identifiers!: AsyncHasMany<IdentifierModel>;
192

193
    @hasMany('subject', { inverse: null, async: false })
194
    subjects!: SubjectModel[];
195

196
    // These are only computeds because maintaining separate flag values on
197
    // different classes would be a headache TODO: Improve.
198

199
    /**
200
     * Is this a project? Flag can be used to provide template-specific behavior
201
     * for different resource types.
202
     */
203
    @equal('constructor.modelName', 'node') isProject!: boolean;
204

205
    /**
206
     * Is this a registration? Flag can be used to provide template-specific
207
     * behavior for different resource types.
208
     */
209
    @equal('constructor.modelName', 'registration') isRegistration!: boolean;
210

211
    /**
212
     * Is this node being viewed through an anonymized, view-only link?
213
     */
214
    @bool('apiMeta.anonymous') isAnonymous!: boolean;
215

216
    /**
217
     * Does the current user have write permission on this node?
218
     */
219
    @computed('currentUserPermissions')
220
    get userHasWritePermission() {
221
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Write);
216✔
222
    }
223

224
    /**
225
     * Is the current user an admin on this node?
226
     */
227
    @computed('currentUserPermissions')
228
    get userHasAdminPermission() {
229
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Admin);
175✔
230
    }
231

232
    /**
233
     * Does the current user have read permission on this node?
234
     */
235
    @computed('currentUserPermissions')
236
    get userHasReadPermission() {
237
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read);
157✔
238
    }
239

240
    @computed('currentUserPermissions.length')
241
    get currentUserIsReadOnly() {
242
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read)
16✔
243
            && this.currentUserPermissions.length === 1;
244
    }
245

246
    /**
247
     * The type of this node.
248
     */
249
    @computed('isFork', 'isRegistration')
250
    get nodeType(): NodeType {
251
        if (this.isRegistration) {
67✔
252
            return NodeType.Registration;
38✔
253
        }
254
        if (this.isFork) {
29✔
255
            return NodeType.Fork;
28✔
256
        }
257
        return NodeType.Generic;
1✔
258
    }
259

260
    /**
261
     * The type of this node, as a string.
262
     */
263
    get nodeTypeTranslation(): string {
264
        let translationNode = this.isRoot ? 'project' : 'component';
×
265
        if (this.isRegistration) {
×
266
            translationNode = 'registration';
×
267
        }
268
        return this.intl.t(`general.${translationNode}`);
×
269
    }
270

271
    // This is for the title helper, which does its own encoding of unsafe characters
272
    @computed('title')
273
    get unsafeTitle() {
274
        return htmlSafe(this.title);
33✔
275
    }
276

277
    @computed('id', 'root')
278
    get isRoot() {
279
        const rootId = (this as NodeModel).belongsTo('root').id();
110✔
280
        return !rootId || rootId === this.id;
110✔
281
    }
282

283
    // BaseFileItem override
284
    isNode = true;
572✔
285
    collectable = false;
572✔
286

287
    makeFork(): Promise<object> {
288
        const url = getRelatedHref(this.links.relationships!.forks);
3✔
289
        return this.currentUser.authenticatedAJAX({
3✔
290
            url,
291
            type: 'POST',
292
            headers: {
293
                'Content-Type': 'application/json',
294
            },
295
            data: JSON.stringify({
296
                data: { type: 'nodes' },
297
            }),
298
        });
299
    }
300

301
    /**
302
     * Sets the nodeLicense field defaults based on required fields from a License
303
     */
304
    setNodeLicenseDefaults(requiredFields: Array<keyof NodeLicense>): void {
305
        if (!requiredFields.length && this.nodeLicense) {
×
306
            // If the nodeLicense exists, notify property change so that validation is triggered
307
            this.notifyPropertyChange('nodeLicense');
×
308

309
            return;
×
310
        }
311

312
        const {
313
            copyrightHolders = '',
×
314
            year = new Date().getUTCFullYear().toString(),
×
315
        } = (this.nodeLicense || {});
×
316

317
        const nodeLicenseDefaults: NodeLicense = {
×
318
            copyrightHolders,
319
            year,
320
        };
321

322
        // Only set the required fields on nodeLicense
323
        const props = requiredFields.reduce(
×
324
            (acc, val) => ({ ...acc, [val]: nodeLicenseDefaults[val] }),
×
325
            {},
326
        );
327

328
        this.set('nodeLicense', props);
×
329
    }
330

331
    @task
332
    @waitFor
333
    async getEnabledAddons() {
334
        const endpoint = `${apiUrl}/${apiNamespace}/nodes/${this.id}/addons/`;
21✔
335
        const response = await this.currentUser.authenticatedAJAX({
21✔
336
            url: endpoint,
337
            type: 'GET',
338
            headers: {
339
                'Content-Type': 'application/json',
340
            },
341
            xhrFields: { withCredentials: true },
342
        });
343
        if (response.data) {
21✔
344
            const addonList = response.data
11✔
345
                .filter((addon: any) => addon.attributes.node_has_auth)
10✔
346
                .map((addon: any) => addon.id);
10✔
347
            this.set('addonsEnabled', addonList);
11✔
348
        }
349
    }
350
}
351

352
declare module 'ember-data/types/registries/model' {
353
    export default interface ModelRegistry {
354
        node: NodeModel;
355
    } // eslint-disable-line semi
356
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc