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

CenterForOpenScience / ember-osf-web / 13033595221

29 Jan 2025 02:33PM UTC coverage: 66.835% (-1.2%) from 68.015%
13033595221

push

github

web-flow
Merge Feature/addon services into develop (#2492)

* Add addon service routes (#2042)

## Purpose
- Add routes and placeholder templates for new addon service routes

## Summary of Changes
- Update app/router.ts
- Add route and template file for new routes
- Add unit tests for new routes

* [ENG-4687] Add v2 models, adapters, and serializers (#2045)

Purpose

Add models, adapters, and serializers for various v2 endpoints we'll need for addons. Note that some of this does not reflect the BE as it exists today, but is a bit aspirational. That being said, it does not include all the changes we ultimately want, just the ones that are necessary to make this function in a sane manner.

Summary of Changes

Add addon, external-account, node-addon, and user-addon models, adapters, and serializers

* [ENG-4688] Addon service models (#2048)

* Add config variable for new addon service

* WIP models for addon service

* More WIP models

* WIP

* Remove unneeded model

* [ENG-4681] Add mirage for v2 api (#2051)

## Purpose

Make the v2 endpoints work with mirage. This includes a lot of normalization of the API but not the extra features such as extended attributes for providers nor getting the folder lists.

## Summary of Changes

1. Add mirage
2. Adjust models

* [ENG-4682] Mirage for addons (#2062)

* Implement basic management component and providers (#2084)

Purpose

Start the development of the management component. This includes the mixing and sorting of the various provider types, creating abstracted providers for each api, and filling out some of the v2 api functionality.

Summary of Changes

Create management component
Create Provider and LegacyProvider
Add parent relationships to some models
Modify the addon model for the new way of serializing the addon category

* [ENG-4964] Addon cards (#2092)

## Purpose
- Add addon-cards component to configure, enable, and disable addons

## Summary of C... (continued)

3106 of 5066 branches covered (61.31%)

Branch coverage included in aggregate %.

453 of 809 new or added lines in 38 files covered. (56.0%)

9 existing lines in 3 files now uncovered.

7899 of 11400 relevant lines covered (69.29%)

189.68 hits per line

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

62.32
/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
import captureException from 'ember-osf-web/utils/capture-exception';
17

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

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

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

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

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

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

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

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

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

124
    @hasMany('subject', { inverse: null, async: false }) subjectsAcceptable?: SubjectModel[];
125

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

130
    @hasMany('contributor', { inverse: 'node' })
131
    contributors!: AsyncHasMany<ContributorModel> & ContributorModel[];
132

133
    @hasMany('contributor', { inverse: null })
134
    bibliographicContributors!: AsyncHasMany<ContributorModel>;
135

136
    @belongsTo('node', { inverse: 'children' })
137
    parent!: AsyncBelongsTo<NodeModel> & NodeModel;
138

139
    @belongsTo('region')
140
    region!: RegionModel;
141

142
    @hasMany('node', { inverse: 'parent' })
143
    children!: AsyncHasMany<NodeModel>;
144

145
    @hasMany('preprint', { inverse: 'node' })
146
    preprints!: AsyncHasMany<PreprintModel>;
147

148
    @hasMany('institution', { inverse: 'nodes' })
149
    affiliatedInstitutions!: AsyncHasMany<InstitutionModel> | InstitutionModel[];
150

151
    @hasMany('comment', { inverse: 'node' })
152
    comments!: AsyncHasMany<CommentModel>;
153

154
    @belongsTo('citation')
155
    citation!: AsyncBelongsTo<CitationModel> & CitationModel;
156

157
    @belongsTo('license', { inverse: null })
158
    license!: AsyncBelongsTo<LicenseModel> & LicenseModel;
159

160
    @hasMany('node', { inverse: null })
161
    linkedNodes!: AsyncHasMany<NodeModel> & NodeModel[];
162

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

166
    @hasMany('registration', { inverse: 'registeredFrom' })
167
    registrations!: AsyncHasMany<RegistrationModel>;
168

169
    @hasMany('node', { inverse: 'forkedFrom', polymorphic: true })
170
    forks!: AsyncHasMany<NodeModel>;
171

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

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

178
    @belongsTo('node-storage', { inverse: null })
179
    storage!: AsyncBelongsTo<NodeStorageModel> & NodeStorageModel;
180

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

184
    @hasMany('node', { inverse: null })
185
    linkedByRegistrations!: AsyncHasMany<RegistrationModel>;
186

187
    @hasMany('wiki', { inverse: 'node' })
188
    wikis!: AsyncHasMany<WikiModel>;
189

190
    @hasMany('log', { inverse: 'originalNode' })
191
    logs!: AsyncHasMany<LogModel>;
192

193
    @hasMany('identifier', { inverse: 'referent' })
194
    identifiers!: AsyncHasMany<IdentifierModel>;
195

196
    @hasMany('subject', { inverse: null, async: false })
197
    subjects!: SubjectModel[];
198

199
    // These are only computeds because maintaining separate flag values on
200
    // different classes would be a headache TODO: Improve.
201

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

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

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

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

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

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

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

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

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

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

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

286
    // BaseFileItem override
287
    isNode = true;
581✔
288
    collectable = false;
581✔
289

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

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

312
            return;
×
313
        }
314

315
        const {
316
            copyrightHolders = '',
×
317
            year = new Date().getUTCFullYear().toString(),
×
318
        } = (this.nodeLicense || {});
×
319

320
        const nodeLicenseDefaults: NodeLicense = {
×
321
            copyrightHolders,
322
            year,
323
        };
324

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

331
        this.set('nodeLicense', props);
×
332
    }
333

334
    @task
335
    @waitFor
336
    async getEnabledAddons() {
337
        try {
11✔
338
            const endpoint = `${apiUrl}/${apiNamespace}/nodes/${this.id}/addons/`;
11✔
339
            const response = await this.currentUser.authenticatedAJAX({
11✔
340
                url: endpoint,
341
                type: 'GET',
342
                headers: {
343
                    'Content-Type': 'application/json',
344
                },
345
                xhrFields: { withCredentials: true },
346
            });
347
            if (response.data) {
11!
348
                const addonList = response.data
11✔
349
                    .filter((addon: any) => addon.attributes.node_has_auth)
9✔
350
                    .map((addon: any) => addon.id);
9✔
351
                this.set('addonsEnabled', addonList);
11✔
352
            }
353
        } catch (e) {
NEW
354
            captureException(e);
×
355
        }
356
    }
357
}
358

359
declare module 'ember-data/types/registries/model' {
360
    export default interface ModelRegistry {
361
        node: NodeModel;
362
    } // eslint-disable-line semi
363
}
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