adapt-authoring-core 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/adapt-authoring.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"module": false,
|
|
3
3
|
"documentation": {
|
|
4
4
|
"enable": true,
|
|
5
|
+
"manualCover": "docs/cover-manual.md",
|
|
5
6
|
"manualIndex": "docs/index-manual.md",
|
|
6
7
|
"sourceIndex": "docs/index-backend.md",
|
|
7
8
|
"manualPages": {
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"manualPlugins": [
|
|
23
24
|
"docs/plugins/binscripts.js",
|
|
24
25
|
"docs/plugins/coremodules.js",
|
|
26
|
+
"docs/plugins/index-manual.js",
|
|
25
27
|
"docs/plugins/licensing.js"
|
|
26
28
|
]
|
|
27
29
|
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import https from 'https'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Documentation plugin that generates a list of contributors
|
|
5
|
+
* from all adapt-authoring-* repositories in the adapt-security organisation.
|
|
6
|
+
*/
|
|
7
|
+
export default class Contributors {
|
|
8
|
+
ICON_SIZE = 55
|
|
9
|
+
BORDER_WIDTH = 3
|
|
10
|
+
MIN_CONTRIBUTIONS = 25
|
|
11
|
+
|
|
12
|
+
constructor (app, config, outputDir) {
|
|
13
|
+
this.org = 'adapt-security'
|
|
14
|
+
this.repoPrefix = 'adapt-authoring'
|
|
15
|
+
this.perPage = 100
|
|
16
|
+
this.excludeUsers = ['dependabot[bot]', 'dependabot-preview[bot]', 'greenkeeper[bot]', 'semantic-release-bot', 'snyk-bot']
|
|
17
|
+
// Contribution tiers
|
|
18
|
+
this.tiers = [
|
|
19
|
+
{ name: 'gold', count: 3, border: '#FFD700' },
|
|
20
|
+
{ name: 'silver', count: 6, border: '#C0C0C0' },
|
|
21
|
+
{ name: 'bronze', count: 10, border: '#CD7F32' },
|
|
22
|
+
{ name: 'contributor' }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async run () {
|
|
27
|
+
this.manualFile = 'index-manual.md'
|
|
28
|
+
this.replace = { CONTRIBUTORS: await this.generateContributorsList() }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Makes an HTTPS GET request to the GitHub API
|
|
33
|
+
*/
|
|
34
|
+
async githubRequest (path) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const options = {
|
|
37
|
+
hostname: 'api.github.com',
|
|
38
|
+
path,
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: {
|
|
41
|
+
'User-Agent': 'adapt-authoring-docs',
|
|
42
|
+
Accept: 'application/vnd.github.v3+json'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add auth token if available (increases rate limit)
|
|
47
|
+
if (process.env.GITHUB_TOKEN) {
|
|
48
|
+
options.headers.Authorization = `token ${process.env.GITHUB_TOKEN}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const req = https.request(options, res => {
|
|
52
|
+
let data = ''
|
|
53
|
+
res.on('data', chunk => { data += chunk })
|
|
54
|
+
res.on('end', () => {
|
|
55
|
+
try {
|
|
56
|
+
resolve(JSON.parse(data))
|
|
57
|
+
} catch (e) {
|
|
58
|
+
reject(new Error(`Failed to parse GitHub response: ${e.message}`))
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
req.on('error', reject)
|
|
64
|
+
req.end()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fetches all repositories matching the prefix from the organisation
|
|
70
|
+
*/
|
|
71
|
+
async fetchRepos () {
|
|
72
|
+
const repos = []
|
|
73
|
+
let page = 1
|
|
74
|
+
|
|
75
|
+
while (true) {
|
|
76
|
+
const response = await this.githubRequest(
|
|
77
|
+
`/orgs/${this.org}/repos?per_page=${this.perPage}&page=${page}`
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (!Array.isArray(response) || response.length === 0) break
|
|
81
|
+
|
|
82
|
+
const matchingRepos = response
|
|
83
|
+
.filter(repo => repo.name.startsWith(this.repoPrefix))
|
|
84
|
+
.map(repo => repo.name)
|
|
85
|
+
|
|
86
|
+
repos.push(...matchingRepos)
|
|
87
|
+
|
|
88
|
+
if (response.length < this.perPage) break
|
|
89
|
+
page++
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return repos
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetches contributors for a single repository
|
|
97
|
+
*/
|
|
98
|
+
async fetchRepoContributors (repoName) {
|
|
99
|
+
const contributors = []
|
|
100
|
+
let page = 1
|
|
101
|
+
|
|
102
|
+
while (true) {
|
|
103
|
+
const response = await this.githubRequest(
|
|
104
|
+
`/repos/${this.org}/${repoName}/contributors?per_page=${this.perPage}&page=${page}`
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (!Array.isArray(response) || response.length === 0) break
|
|
108
|
+
|
|
109
|
+
contributors.push(...response)
|
|
110
|
+
|
|
111
|
+
if (response.length < this.perPage) break
|
|
112
|
+
page++
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return contributors
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Aggregates contributors across all repositories
|
|
120
|
+
*/
|
|
121
|
+
async aggregateContributors () {
|
|
122
|
+
const repos = await this.fetchRepos()
|
|
123
|
+
const contributorMap = new Map()
|
|
124
|
+
|
|
125
|
+
for (const repo of repos) {
|
|
126
|
+
try {
|
|
127
|
+
const contributors = await this.fetchRepoContributors(repo)
|
|
128
|
+
|
|
129
|
+
for (const contributor of contributors) {
|
|
130
|
+
if (this.excludeUsers.includes(contributor.login)) continue
|
|
131
|
+
if (contributor.type !== 'User') continue
|
|
132
|
+
|
|
133
|
+
if (contributorMap.has(contributor.login)) {
|
|
134
|
+
const existing = contributorMap.get(contributor.login)
|
|
135
|
+
existing.contributions += contributor.contributions
|
|
136
|
+
} else {
|
|
137
|
+
contributorMap.set(contributor.login, {
|
|
138
|
+
login: contributor.login,
|
|
139
|
+
avatarUrl: contributor.avatar_url,
|
|
140
|
+
profileUrl: contributor.html_url,
|
|
141
|
+
contributions: contributor.contributions
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.warn(`Failed to fetch contributors for ${repo}: ${e.message}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Sort by contributions (descending) and return as array
|
|
151
|
+
return Array.from(contributorMap.values())
|
|
152
|
+
.sort((a, b) => b.contributions - a.contributions)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Determines the tier for a contributor based on rank and contribution count
|
|
157
|
+
*/
|
|
158
|
+
getTier (contributor, rank) {
|
|
159
|
+
let position = 0
|
|
160
|
+
for (const tier of this.tiers) {
|
|
161
|
+
const tierMax = position + (tier.count || Infinity)
|
|
162
|
+
const rankMatch = !tier.count || (rank > position && rank <= tierMax)
|
|
163
|
+
const minContributions = tier.count ? this.MIN_CONTRIBUTIONS : 0
|
|
164
|
+
const countMatch = contributor.contributions >= minContributions
|
|
165
|
+
|
|
166
|
+
if (rankMatch && countMatch) {
|
|
167
|
+
return tier
|
|
168
|
+
}
|
|
169
|
+
position = tierMax
|
|
170
|
+
}
|
|
171
|
+
return this.tiers[this.tiers.length - 1]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generates the HTML for a single contributor avatar
|
|
176
|
+
*/
|
|
177
|
+
contributorToHtml (contributor, tier) {
|
|
178
|
+
const hexClipPath = 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)'
|
|
179
|
+
const isHexagon = tier.name !== 'contributor'
|
|
180
|
+
|
|
181
|
+
const wrapperStyle = [
|
|
182
|
+
`width: ${this.ICON_SIZE}px`,
|
|
183
|
+
`height: ${this.ICON_SIZE}px`,
|
|
184
|
+
isHexagon ? `clip-path: ${hexClipPath}` : 'border-radius: 50%',
|
|
185
|
+
tier.border ? `background: ${tier.border}` : '',
|
|
186
|
+
'display: inline-block',
|
|
187
|
+
'transition: transform 0.2s'
|
|
188
|
+
].filter(Boolean).join('; ')
|
|
189
|
+
|
|
190
|
+
const imgSize = this.ICON_SIZE - (this.BORDER_WIDTH * 2)
|
|
191
|
+
|
|
192
|
+
const imgStyle = [
|
|
193
|
+
`width: ${imgSize}px`,
|
|
194
|
+
`height: ${imgSize}px`,
|
|
195
|
+
isHexagon ? `clip-path: ${hexClipPath}` : 'border-radius: 50%',
|
|
196
|
+
'display: block',
|
|
197
|
+
`margin: ${this.BORDER_WIDTH}px`
|
|
198
|
+
].join('; ')
|
|
199
|
+
|
|
200
|
+
return `<a href="${contributor.profileUrl}" title="${contributor.login} (${contributor.contributions} contributions)" target="_blank" rel="noopener" style="${wrapperStyle}"><img src="${contributor.avatarUrl}" alt="${contributor.login}" class="contributor-avatar contributor-${tier.name}" style="${imgStyle}" /></a>`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Generates the full contributors list HTML
|
|
205
|
+
*/
|
|
206
|
+
async generateContributorsList () {
|
|
207
|
+
try {
|
|
208
|
+
const contributors = await this.aggregateContributors()
|
|
209
|
+
|
|
210
|
+
if (contributors.length === 0) {
|
|
211
|
+
return '<p>No contributors found.</p>'
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Group contributors by tier
|
|
215
|
+
const tierGroups = new Map()
|
|
216
|
+
contributors.forEach((contributor, index) => {
|
|
217
|
+
const tier = this.getTier(contributor, index + 1)
|
|
218
|
+
if (!tierGroups.has(tier.name)) {
|
|
219
|
+
tierGroups.set(tier.name, [])
|
|
220
|
+
}
|
|
221
|
+
tierGroups.get(tier.name).push(this.contributorToHtml(contributor, tier))
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Generate HTML for each tier row
|
|
225
|
+
const rows = Array.from(tierGroups.entries())
|
|
226
|
+
.map(([tierName, avatars]) =>
|
|
227
|
+
`<div class="contributors-row contributors-${tierName}">\n${avatars.join('\n')}\n</div>`
|
|
228
|
+
)
|
|
229
|
+
.join('\n')
|
|
230
|
+
|
|
231
|
+
return `<div class="contributors-grid">\n${rows}\n</div>
|
|
232
|
+
<style>
|
|
233
|
+
.contributors-grid {
|
|
234
|
+
margin: 0 auto;
|
|
235
|
+
max-width: 600px;
|
|
236
|
+
padding: 20px 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.contributors-row {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
align-items: center;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
gap: 4px;
|
|
245
|
+
margin-bottom: 12px;
|
|
246
|
+
text-align: center;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.contributors-grid a:hover {
|
|
250
|
+
transform: scale(1.1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.contributor-avatar {
|
|
254
|
+
object-fit: cover;
|
|
255
|
+
vertical-align: middle;
|
|
256
|
+
}
|
|
257
|
+
</style>`
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.error('Failed to generate contributors list:', e)
|
|
260
|
+
return '<p>Unable to load contributors list.</p>'
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Adapt Authoring Tool Developer guides
|
|
2
|
+
|
|
3
|
+
Welcome to the technical documentation for the Adapt authoring tool — a web-based application for creating responsive, multi-device e-learning content built on the [Adapt Framework](https://github.com/adaptlearning/adapt_framework).
|
|
4
|
+
|
|
5
|
+
The authoring tool provides a user-friendly interface for building courses without needing to write code, while its modular architecture gives developers the flexibility to extend and customise virtually every aspect of the system. Whether you're looking to build custom plugins, integrate with external services, or contribute to the core codebase, you're in the right place.
|
|
6
|
+
|
|
7
|
+
## What makes it tick
|
|
8
|
+
|
|
9
|
+
The authoring tool is built on a few key principles:
|
|
10
|
+
|
|
11
|
+
- **Modular by design** — The entire application is composed of discrete modules that can be swapped, extended, or replaced. Need custom authentication? Write an auth plugin. Want to store data differently? Create a new database adapter.
|
|
12
|
+
|
|
13
|
+
- **Schema-driven** — Content types, validation rules, and UI forms are all defined using JSON schemas. This means you can add new content types or modify existing ones without touching application code.
|
|
14
|
+
|
|
15
|
+
- **RESTful API** — Every feature is accessible via a comprehensive REST API, making it straightforward to integrate with other systems or build custom tooling.
|
|
16
|
+
|
|
17
|
+
- **Built for collaboration** — Multi-user support with role-based permissions lets teams work together on courses with appropriate access controls.
|
|
18
|
+
|
|
19
|
+
## About this documentation
|
|
20
|
+
|
|
21
|
+
This documentation covers the technical side of the authoring tool — how it works under the hood and how to extend it. You'll find guides on writing custom modules, working with the database, creating schemas, and contributing to the project.
|
|
22
|
+
|
|
23
|
+
If you're looking for help using the authoring tool to create courses, check out the user guides on the [Adapt Learning community site](https://www.adaptlearning.org/).
|
|
24
|
+
|
|
25
|
+
## Where to start
|
|
26
|
+
|
|
27
|
+
New to the codebase? Here are some good starting points:
|
|
28
|
+
|
|
29
|
+
- **[Folder Structure](folder-structure)** — Get familiar with how the application is organised
|
|
30
|
+
- **[Writing a Module](writing-a-module)** — Learn the basics of creating your own module
|
|
31
|
+
- **[Schemas Introduction](schemas-introduction)** — Understand how schemas drive the application
|
|
32
|
+
- **[Hooks](hooks)** — See how to tap into the application lifecycle
|
|
33
|
+
|
|
34
|
+
## Get involved
|
|
35
|
+
|
|
36
|
+
The Adapt authoring tool is open source and we welcome contributions. You can find the source code and report issues on GitHub:
|
|
37
|
+
|
|
38
|
+
- [adapt-security](https://github.com/adapt-security) — Authoring tool repositories
|
|
39
|
+
- [adaptlearning](https://github.com/adaptlearning) — Adapt Framework and community plugins
|
|
40
|
+
|
|
41
|
+
## Contributors
|
|
42
|
+
|
|
43
|
+
A huge thank you to everyone who has contributed to the Adapt authoring tool. This project wouldn't be possible without the time and effort of our community.
|
|
44
|
+
|
|
45
|
+
{{{CONTRIBUTORS}}}
|
|
46
|
+
|
|
47
|
+
<div class="big-text">Happy coding!</div>
|
package/package.json
CHANGED
|
File without changes
|