Merge remote-tracking branch 'origin/main' into dlaliberte-intentpreview-1030
|
@ -0,0 +1,63 @@
|
|||
# Copyright 2024 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from chromestatus_openapi.models.feature_link import FeatureLink
|
||||
from chromestatus_openapi.models.spec_mentor import SpecMentor
|
||||
|
||||
from framework import basehandlers
|
||||
from internals.core_models import FeatureEntry
|
||||
|
||||
|
||||
class SpecMentorsAPI(basehandlers.APIHandler):
|
||||
"""Implements the OpenAPI /spec_mentors path."""
|
||||
|
||||
def do_get(self, **kwargs):
|
||||
"""Get a list of matching spec mentors.
|
||||
|
||||
Returns:
|
||||
A list of data on all public origin trials.
|
||||
"""
|
||||
after_param: str | None = self.request.args.get('after', None)
|
||||
after: datetime | None = None
|
||||
if after_param is not None:
|
||||
try:
|
||||
after = datetime.fromisoformat(after_param)
|
||||
except ValueError:
|
||||
self.abort(400, f'invalid ?after parameter {after_param}')
|
||||
|
||||
# Every non-empty string is greater than '', so this will find all entries with any spec mentors
|
||||
# set. (!= is interpreted as "less or greater".)
|
||||
# We can't have NDB sort the results because it can only sort by the inequality condition.
|
||||
query = FeatureEntry.query(FeatureEntry.spec_mentor_emails > '')
|
||||
features: list[FeatureEntry] = query.fetch()
|
||||
if after is not None:
|
||||
# Do this in Python rather than the NDB query because NDB queries only support one inequality.
|
||||
features = [feature for feature in features if feature.updated > after]
|
||||
features.sort(key=lambda f: f.updated, reverse=True)
|
||||
|
||||
mentors: dict[str, list[FeatureLink]] = {}
|
||||
for feature in features:
|
||||
if feature.unlisted:
|
||||
# TODO: Consider showing these when the caller is logged in and has the right to see them.
|
||||
continue
|
||||
for mentor in feature.spec_mentor_emails:
|
||||
mentors.setdefault(mentor,
|
||||
[]).append(FeatureLink(id=feature.key.integer_id(), name=feature.name))
|
||||
|
||||
return [
|
||||
SpecMentor(email, features).to_dict()
|
||||
for email, features in sorted(mentors.items())
|
||||
]
|
|
@ -0,0 +1,260 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import testing_config # isort: split
|
||||
|
||||
import json
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
|
||||
import flask
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from api import features_api, spec_mentors_api
|
||||
from framework import rediscache
|
||||
from internals import user_models
|
||||
from internals.core_models import FeatureEntry
|
||||
|
||||
test_app = flask.Flask(__name__)
|
||||
|
||||
BASE_FEATURE_CREATE_BODY = {
|
||||
'name': 'A name',
|
||||
'summary': 'A summary',
|
||||
'owner_emails': 'user@example.com',
|
||||
'category': 2,
|
||||
'feature_type': 1,
|
||||
'impl_status_chrome': 3,
|
||||
'standard_maturity': 2,
|
||||
'ff_views': 1,
|
||||
'safari_views': 1,
|
||||
'web_dev_views': 1,
|
||||
}
|
||||
|
||||
|
||||
class SpecMentorsAPITest(testing_config.CustomTestCase):
|
||||
def setUp(self):
|
||||
self.api_base = '/api/v0'
|
||||
self.request_path = f'{self.api_base}/spec_mentors'
|
||||
self.spec_mentors_handler = spec_mentors_api.SpecMentorsAPI()
|
||||
|
||||
self.feature_handler = features_api.FeaturesAPI()
|
||||
|
||||
self.app_admin = user_models.AppUser(email='admin@example.com')
|
||||
self.app_admin.is_admin = True
|
||||
self.app_admin.put()
|
||||
|
||||
self.created_features = []
|
||||
|
||||
def tearDown(self):
|
||||
for feature in self.created_features:
|
||||
feature.key.delete()
|
||||
self.app_admin.key.delete()
|
||||
testing_config.sign_out()
|
||||
|
||||
rediscache.delete_keys_with_prefix('features|*')
|
||||
rediscache.delete_keys_with_prefix('FeatureEntries|*')
|
||||
|
||||
def createFeature(self, params) -> FeatureEntry:
|
||||
with test_app.test_request_context(
|
||||
f'{self.api_base}/features/create', json=BASE_FEATURE_CREATE_BODY | params
|
||||
):
|
||||
response = self.feature_handler.do_post()
|
||||
feature = FeatureEntry.get_by_id(response['feature_id'])
|
||||
self.created_features.append(feature)
|
||||
return feature
|
||||
|
||||
def test_finds_one_mentored_feature(self):
|
||||
"""Returns the single feature that exists, which has a mentor."""
|
||||
# Create a feature using the admin user.
|
||||
testing_config.sign_in(self.app_admin.email, 123567890)
|
||||
|
||||
feature = self.createFeature({'name': 'The feature'})
|
||||
feature.spec_mentor_emails = ['mentor@example.org']
|
||||
feature.put()
|
||||
testing_config.sign_out()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
|
||||
self.assertEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
'email': 'mentor@example.org',
|
||||
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_omits_unlisted_feature(self):
|
||||
"""Does not return an unlisted feature that has a mentor."""
|
||||
# Create a feature using the admin user.
|
||||
testing_config.sign_in(self.app_admin.email, 123567890)
|
||||
|
||||
feature = self.createFeature({'name': 'The feature'})
|
||||
feature.spec_mentor_emails = ['mentor@example.org']
|
||||
feature.unlisted = True
|
||||
feature.put()
|
||||
testing_config.sign_out()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
|
||||
self.assertEqual(response, [])
|
||||
|
||||
def test_obeys_after_param(self):
|
||||
"""The ?after URL parameter filters features."""
|
||||
# Create a feature using the admin user.
|
||||
testing_config.sign_in(self.app_admin.email, 123567890)
|
||||
|
||||
feature = self.createFeature({'name': 'The feature'})
|
||||
feature.spec_mentor_emails = ['mentor@example.org']
|
||||
feature.updated = datetime.fromisoformat('2024-01-14')
|
||||
feature.put()
|
||||
testing_config.sign_out()
|
||||
|
||||
with test_app.test_request_context(f'{self.request_path}?after=2024-01-15'):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
|
||||
self.assertEqual(response, [])
|
||||
|
||||
# Now the feature was last updated after the 'after' param.
|
||||
feature.updated = datetime.fromisoformat('2024-01-16')
|
||||
feature.put()
|
||||
|
||||
with test_app.test_request_context(f'{self.request_path}?after=2024-01-15'):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
self.assertEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
'email': 'mentor@example.org',
|
||||
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_fails_on_malformed_after_param(self):
|
||||
"""An ?after value that isn't a date returns a 400 error."""
|
||||
with test_app.test_request_context(f'{self.request_path}?after=arglebargle'):
|
||||
with self.assertRaises(HTTPException) as cm:
|
||||
self.spec_mentors_handler.do_get()
|
||||
self.assertEqual(cm.exception.code, 400)
|
||||
|
||||
def test_sorts_mentors_alphabetically(self):
|
||||
# Create a feature using the admin user.
|
||||
testing_config.sign_in(self.app_admin.email, 123567890)
|
||||
|
||||
feature = self.createFeature({'name': 'The feature'})
|
||||
feature.spec_mentor_emails = ['bmentor@example.org', 'amentor@example.org']
|
||||
feature.put()
|
||||
testing_config.sign_out()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
|
||||
self.assertEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
'email': 'amentor@example.org',
|
||||
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
||||
},
|
||||
{
|
||||
'email': 'bmentor@example.org',
|
||||
'mentored_features': [{'id': feature.key.id(), 'name': 'The feature'}],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_sorts_features_by_updated_date_recent_first(self):
|
||||
# Create a feature using the admin user.
|
||||
testing_config.sign_in(self.app_admin.email, 123567890)
|
||||
|
||||
feature1 = self.createFeature({'name': 'First feature'})
|
||||
feature1.spec_mentor_emails = ['mentor@example.org']
|
||||
feature1.put()
|
||||
|
||||
feature2 = self.createFeature({'name': 'Second feature'})
|
||||
feature2.spec_mentor_emails = ['mentor@example.org']
|
||||
feature2.put()
|
||||
testing_config.sign_out()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
self.assertEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
'email': 'mentor@example.org',
|
||||
'mentored_features': [
|
||||
# The later-created feature is listed first.
|
||||
{'id': feature2.key.id(), 'name': 'Second feature'},
|
||||
{'id': feature1.key.id(), 'name': 'First feature'},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
# Make the first feature more-recently updated.
|
||||
feature1.updated = datetime.now()
|
||||
feature1.put()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
self.assertEqual(
|
||||
response,
|
||||
[
|
||||
{
|
||||
'email': 'mentor@example.org',
|
||||
'mentored_features': [
|
||||
# And the order switches.
|
||||
{'id': feature1.key.id(), 'name': 'First feature'},
|
||||
{'id': feature2.key.id(), 'name': 'Second feature'},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_organizes_features_by_mentor(self):
|
||||
# Create a feature using the admin user.
|
||||
testing_config.sign_in(self.app_admin.email, 123567890)
|
||||
|
||||
feature1 = self.createFeature({'name': 'First feature'})
|
||||
feature1.spec_mentor_emails = ['mentor@example.org', 'expert@example.com']
|
||||
feature1.put()
|
||||
|
||||
feature2 = self.createFeature({'name': 'Second feature'})
|
||||
feature2.spec_mentor_emails = ['mentor@example.org']
|
||||
feature2.put()
|
||||
testing_config.sign_out()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
response = self.spec_mentors_handler.do_get()
|
||||
|
||||
# Unlike the other test expectations in this file, this one is saved to a JSON file so the
|
||||
# Playwright tests can use it as a mock API response. Because the real feature IDs are
|
||||
# dynamically generated, we have to slot them into the right places here.
|
||||
with open(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'../packages/playwright/tests/spec_mentor_api_result.json',
|
||||
)
|
||||
) as f:
|
||||
expected_response = json.load(f)
|
||||
expected_response[0]['mentored_features'][0]['id'] = feature1.key.id()
|
||||
expected_response[1]['mentored_features'][0]['id'] = feature2.key.id()
|
||||
expected_response[1]['mentored_features'][1]['id'] = feature1.key.id()
|
||||
|
||||
self.assertEqual(response, expected_response)
|
|
@ -93,6 +93,7 @@ import './elements/chromedash-textarea';
|
|||
import './elements/chromedash-timeline';
|
||||
import './elements/chromedash-timeline-page';
|
||||
import './elements/chromedash-toast';
|
||||
import './elements/chromedash-report-spec-mentors-page';
|
||||
import './elements/chromedash-roadmap';
|
||||
import './elements/chromedash-roadmap-milestone-card';
|
||||
import './elements/chromedash-roadmap-page';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {LitElement, css, html, nothing} from 'lit';
|
||||
import {ref, createRef} from 'lit/directives/ref.js';
|
||||
import {styleMap} from 'lit-html/directives/style-map.js';
|
||||
import {showToastMessage, parseRawQuery, updateURLParams, IS_MOBILE} from './utils';
|
||||
import {createRef, ref} from 'lit/directives/ref.js';
|
||||
import page from 'page';
|
||||
import {SHARED_STYLES} from '../css/shared-css.js';
|
||||
import {DRAWER_WIDTH_PX} from './chromedash-drawer.js';
|
||||
import {IS_MOBILE, isoDateString, parseRawQuery, showToastMessage, updateURLParams} from './utils';
|
||||
|
||||
|
||||
class ChromedashApp extends LitElement {
|
||||
|
@ -426,6 +426,14 @@ class ChromedashApp extends LitElement {
|
|||
page('/metrics/css/timeline/animated', () => page.redirect('/metrics/css/animated'));
|
||||
page('/metrics/feature/timeline/popularity', () =>
|
||||
page.redirect('/metrics/feature/popularity'));
|
||||
page('/reports/spec_mentors', (ctx)=> {
|
||||
if (!this.setupNewPage(ctx, 'chromedash-report-spec-mentors-page')) return;
|
||||
this.pageComponent.rawQuery = parseRawQuery(ctx.querystring);
|
||||
this.pageComponent.addEventListener('afterchanged',
|
||||
e => updateURLParams('after', isoDateString(e.detail.after)));
|
||||
this.currentPage = ctx.path;
|
||||
this.hideSidebar();
|
||||
});
|
||||
page('/enterprise', (ctx) => {
|
||||
if (!this.setupNewPage(ctx, 'chromedash-enterprise-page')) return;
|
||||
this.pageComponent.user = this.user;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {LitElement, html, css, nothing} from 'lit';
|
||||
import {showToastMessage, IS_MOBILE} from './utils';
|
||||
import {LitElement, css, html, nothing} from 'lit';
|
||||
import {SHARED_STYLES} from '../css/shared-css.js';
|
||||
import {IS_MOBILE, showToastMessage} from './utils';
|
||||
|
||||
|
||||
export const DRAWER_WIDTH_PX = 200;
|
||||
|
@ -260,6 +260,9 @@ export class ChromedashDrawer extends LitElement {
|
|||
${this.renderNavItem('/metrics/css/popularity', 'CSS')}
|
||||
${this.renderNavItem('/metrics/css/animated', 'CSS Animation')}
|
||||
${this.renderNavItem('/metrics/feature/popularity', 'JS/HTML')}
|
||||
<hr>
|
||||
<div class="section-header">Reports</div>
|
||||
${this.renderNavItem('/reports/spec_mentors', 'Spec Mentors')}
|
||||
|
||||
${adminMenu}
|
||||
</sl-drawer>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
// @ts-check
|
||||
import {LitElement, css, html} from 'lit';
|
||||
import {SHARED_STYLES} from '../css/shared-css.js';
|
||||
import './chromedash-feature-row.js';
|
||||
|
||||
export class ChromedashReportSpecMentor extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
...SHARED_STYLES,
|
||||
css`
|
||||
`];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
mentor: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {import('chromestatus-openapi').SpecMentor} */
|
||||
this.mentor = this.mentor;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<sl-details
|
||||
summary="${this.mentor.email} has mentored:"
|
||||
open>
|
||||
<table>
|
||||
${this.mentor.mentored_features.map(feature => html`
|
||||
<chromedash-feature-row .feature="${feature}"></chromedash-feature-row>
|
||||
`)
|
||||
}
|
||||
</table>
|
||||
</sl-details>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-report-spec-mentor', ChromedashReportSpecMentor);
|
|
@ -0,0 +1,111 @@
|
|||
// @ts-check
|
||||
import {Task} from '@lit/task';
|
||||
import '@shoelace-style/shoelace';
|
||||
import {LitElement, css, html} from 'lit';
|
||||
import {SHARED_STYLES} from '../css/shared-css.js';
|
||||
import './chromedash-report-spec-mentor.js';
|
||||
import {isoDateString} from './utils.js';
|
||||
|
||||
export class ChromedashReportSpecMentorsPage extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
...SHARED_STYLES,
|
||||
css`
|
||||
`];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
rawQuery: {type: Object},
|
||||
_updatedAfter: {state: true},
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
rawQuery;
|
||||
|
||||
/** @type {import('chromestatus-openapi').DefaultApiInterface} */
|
||||
_client;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Default to a year ago.
|
||||
this._updatedAfter = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
|
||||
// @ts-ignore
|
||||
this._client = window.csOpenApiClient;
|
||||
this._mentorsTask = new Task(this, {
|
||||
task: async ([updatedAfter], {signal}) => {
|
||||
const mentors = await this._client.listSpecMentors({after: updatedAfter}, {signal});
|
||||
mentors.sort((a, b) => a.email.localeCompare(b.email));
|
||||
return mentors;
|
||||
},
|
||||
args: () => [this._updatedAfter],
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.initializeParams();
|
||||
}
|
||||
|
||||
initializeParams() {
|
||||
if (!this.rawQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.rawQuery.hasOwnProperty('after')) {
|
||||
const parsed = Date.parse(this.rawQuery['after']);
|
||||
if (!isNaN(parsed)) {
|
||||
this._updatedAfter = new Date(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@shoelace-style/shoelace').SlChangeEvent & {target: import('@shoelace-style/shoelace').SlInput}} e
|
||||
* @return {void}
|
||||
*/
|
||||
afterChanged(e) {
|
||||
e.stopPropagation();
|
||||
const newDate = e.target.valueAsDate;
|
||||
if (newDate) {
|
||||
this._updatedAfter = newDate;
|
||||
this.dispatchEvent(new CustomEvent('afterchanged', {
|
||||
detail: {after: this._updatedAfter},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="subheader">
|
||||
<sl-input
|
||||
type="date"
|
||||
name="after"
|
||||
value=${isoDateString(this._updatedAfter)}
|
||||
label="List spec mentors who've worked on features that were updated after this date"
|
||||
@sl-change=${this.afterChanged}
|
||||
></sl-input>
|
||||
</div>
|
||||
${
|
||||
this._mentorsTask.render({
|
||||
pending: () => html`
|
||||
<details open>
|
||||
<summary><sl-skeleton effect="sheen"></sl-skeleton></summary>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
</details>`,
|
||||
complete: (specMentors) => specMentors.map(mentor =>
|
||||
html`<chromedash-report-spec-mentor .mentor=${mentor}></chromedash-report-spec-mentor>`),
|
||||
error: (e) => {
|
||||
console.error(e);
|
||||
return html`<p>Some errors occurred. Please refresh the page or try again later.</p>`;
|
||||
},
|
||||
})
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-report-spec-mentors-page', ChromedashReportSpecMentorsPage);
|
|
@ -333,6 +333,16 @@ export function renderRelativeDate(dateStr) {
|
|||
}
|
||||
|
||||
|
||||
/** Returns the non-time part of date in the YYYY-MM-DD format.
|
||||
*
|
||||
* @param {Date} date
|
||||
* @return {string}
|
||||
*/
|
||||
export function isoDateString(date) {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses URL query strings into a dict.
|
||||
* @param {string} rawQuery a raw URL query string, e.g. q=abc&num=1;
|
||||
|
|
|
@ -9,7 +9,9 @@ src/index.ts
|
|||
src/models/ComponentUsersRequest.ts
|
||||
src/models/ComponentsUser.ts
|
||||
src/models/ComponentsUsersResponse.ts
|
||||
src/models/FeatureLink.ts
|
||||
src/models/OwnersAndSubscribersOfComponent.ts
|
||||
src/models/SpecMentor.ts
|
||||
src/models/index.ts
|
||||
src/runtime.ts
|
||||
tsconfig.esm.json
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
"prepare": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3"
|
||||
"typescript": "^4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,15 @@ import * as runtime from '../runtime';
|
|||
import type {
|
||||
ComponentUsersRequest,
|
||||
ComponentsUsersResponse,
|
||||
SpecMentor,
|
||||
} from '../models/index';
|
||||
import {
|
||||
ComponentUsersRequestFromJSON,
|
||||
ComponentUsersRequestToJSON,
|
||||
ComponentsUsersResponseFromJSON,
|
||||
ComponentsUsersResponseToJSON,
|
||||
SpecMentorFromJSON,
|
||||
SpecMentorToJSON,
|
||||
} from '../models/index';
|
||||
|
||||
export interface AddUserToComponentRequest {
|
||||
|
@ -31,6 +34,10 @@ export interface AddUserToComponentRequest {
|
|||
componentUsersRequest?: ComponentUsersRequest;
|
||||
}
|
||||
|
||||
export interface ListSpecMentorsRequest {
|
||||
after?: Date;
|
||||
}
|
||||
|
||||
export interface RemoveUserFromComponentRequest {
|
||||
componentId: number;
|
||||
userId: number;
|
||||
|
@ -75,6 +82,21 @@ export interface DefaultApiInterface {
|
|||
*/
|
||||
listComponentUsers(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ComponentsUsersResponse>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary List spec mentors and their activity
|
||||
* @param {Date} [after]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApiInterface
|
||||
*/
|
||||
listSpecMentorsRaw(requestParameters: ListSpecMentorsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<SpecMentor>>>;
|
||||
|
||||
/**
|
||||
* List spec mentors and their activity
|
||||
*/
|
||||
listSpecMentors(requestParameters: ListSpecMentorsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<SpecMentor>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Remove a user from a component
|
||||
|
@ -169,6 +191,36 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface {
|
|||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* List spec mentors and their activity
|
||||
*/
|
||||
async listSpecMentorsRaw(requestParameters: ListSpecMentorsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<SpecMentor>>> {
|
||||
const queryParameters: any = {};
|
||||
|
||||
if (requestParameters.after !== undefined) {
|
||||
queryParameters['after'] = (requestParameters.after as any).toISOString().substring(0,10);
|
||||
}
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
const response = await this.request({
|
||||
path: `/spec_mentors`,
|
||||
method: 'GET',
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
}, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(SpecMentorFromJSON));
|
||||
}
|
||||
|
||||
/**
|
||||
* List spec mentors and their activity
|
||||
*/
|
||||
async listSpecMentors(requestParameters: ListSpecMentorsRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<SpecMentor>> {
|
||||
const response = await this.listSpecMentorsRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from a component
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* chomestatus API
|
||||
* The API for chromestatus.com. chromestatus.com is the official tool used for tracking feature launches in Blink (the browser engine that powers Chrome and many other web browsers). This tool guides feature owners through our launch process and serves as a primary source for developer information that then ripples throughout the web developer ecosystem. More details at: https://github.com/GoogleChrome/chromium-dashboard
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import { exists, mapValues } from '../runtime';
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface FeatureLink
|
||||
*/
|
||||
export interface FeatureLink {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof FeatureLink
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FeatureLink
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the FeatureLink interface.
|
||||
*/
|
||||
export function instanceOfFeatureLink(value: object): boolean {
|
||||
let isInstance = true;
|
||||
isInstance = isInstance && "id" in value;
|
||||
isInstance = isInstance && "name" in value;
|
||||
|
||||
return isInstance;
|
||||
}
|
||||
|
||||
export function FeatureLinkFromJSON(json: any): FeatureLink {
|
||||
return FeatureLinkFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function FeatureLinkFromJSONTyped(json: any, ignoreDiscriminator: boolean): FeatureLink {
|
||||
if ((json === undefined) || (json === null)) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
|
||||
'id': json['id'],
|
||||
'name': json['name'],
|
||||
};
|
||||
}
|
||||
|
||||
export function FeatureLinkToJSON(value?: FeatureLink | null): any {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
|
||||
'id': value.id,
|
||||
'name': value.name,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* chomestatus API
|
||||
* The API for chromestatus.com. chromestatus.com is the official tool used for tracking feature launches in Blink (the browser engine that powers Chrome and many other web browsers). This tool guides feature owners through our launch process and serves as a primary source for developer information that then ripples throughout the web developer ecosystem. More details at: https://github.com/GoogleChrome/chromium-dashboard
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import { exists, mapValues } from '../runtime';
|
||||
import type { FeatureLink } from './FeatureLink';
|
||||
import {
|
||||
FeatureLinkFromJSON,
|
||||
FeatureLinkFromJSONTyped,
|
||||
FeatureLinkToJSON,
|
||||
} from './FeatureLink';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SpecMentor
|
||||
*/
|
||||
export interface SpecMentor {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SpecMentor
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<FeatureLink>}
|
||||
* @memberof SpecMentor
|
||||
*/
|
||||
mentored_features: Array<FeatureLink>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the SpecMentor interface.
|
||||
*/
|
||||
export function instanceOfSpecMentor(value: object): boolean {
|
||||
let isInstance = true;
|
||||
isInstance = isInstance && "email" in value;
|
||||
isInstance = isInstance && "mentored_features" in value;
|
||||
|
||||
return isInstance;
|
||||
}
|
||||
|
||||
export function SpecMentorFromJSON(json: any): SpecMentor {
|
||||
return SpecMentorFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function SpecMentorFromJSONTyped(json: any, ignoreDiscriminator: boolean): SpecMentor {
|
||||
if ((json === undefined) || (json === null)) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
|
||||
'email': json['email'],
|
||||
'mentored_features': ((json['mentored_features'] as Array<any>).map(FeatureLinkFromJSON)),
|
||||
};
|
||||
}
|
||||
|
||||
export function SpecMentorToJSON(value?: SpecMentor | null): any {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
|
||||
'email': value.email,
|
||||
'mentored_features': ((value.mentored_features as Array<any>).map(FeatureLinkToJSON)),
|
||||
};
|
||||
}
|
||||
|
|
@ -3,4 +3,6 @@
|
|||
export * from './ComponentUsersRequest';
|
||||
export * from './ComponentsUser';
|
||||
export * from './ComponentsUsersResponse';
|
||||
export * from './FeatureLink';
|
||||
export * from './OwnersAndSubscribersOfComponent';
|
||||
export * from './SpecMentor';
|
||||
|
|
|
@ -15,7 +15,9 @@ chromestatus_openapi/models/base_model.py
|
|||
chromestatus_openapi/models/component_users_request.py
|
||||
chromestatus_openapi/models/components_user.py
|
||||
chromestatus_openapi/models/components_users_response.py
|
||||
chromestatus_openapi/models/feature_link.py
|
||||
chromestatus_openapi/models/owners_and_subscribers_of_component.py
|
||||
chromestatus_openapi/models/spec_mentor.py
|
||||
chromestatus_openapi/openapi/openapi.yaml
|
||||
chromestatus_openapi/test/__init__.py
|
||||
chromestatus_openapi/test/test_default_controller.py
|
||||
|
|
|
@ -5,6 +5,7 @@ from typing import Union
|
|||
|
||||
from chromestatus_openapi.models.component_users_request import ComponentUsersRequest # noqa: E501
|
||||
from chromestatus_openapi.models.components_users_response import ComponentsUsersResponse # noqa: E501
|
||||
from chromestatus_openapi.models.spec_mentor import SpecMentor # noqa: E501
|
||||
from chromestatus_openapi import util
|
||||
|
||||
|
||||
|
@ -38,6 +39,20 @@ def list_component_users(): # noqa: E501
|
|||
return 'do some magic!'
|
||||
|
||||
|
||||
def list_spec_mentors(after=None): # noqa: E501
|
||||
"""List spec mentors and their activity
|
||||
|
||||
# noqa: E501
|
||||
|
||||
:param after:
|
||||
:type after: str
|
||||
|
||||
:rtype: Union[List[SpecMentor], Tuple[List[SpecMentor], int], Tuple[List[SpecMentor], int, Dict[str, str]]
|
||||
"""
|
||||
after = util.deserialize_date(after)
|
||||
return 'do some magic!'
|
||||
|
||||
|
||||
def remove_user_from_component(component_id, user_id, component_users_request=None): # noqa: E501
|
||||
"""Remove a user from a component
|
||||
|
||||
|
|
|
@ -3,4 +3,6 @@
|
|||
from chromestatus_openapi.models.component_users_request import ComponentUsersRequest
|
||||
from chromestatus_openapi.models.components_user import ComponentsUser
|
||||
from chromestatus_openapi.models.components_users_response import ComponentsUsersResponse
|
||||
from chromestatus_openapi.models.feature_link import FeatureLink
|
||||
from chromestatus_openapi.models.owners_and_subscribers_of_component import OwnersAndSubscribersOfComponent
|
||||
from chromestatus_openapi.models.spec_mentor import SpecMentor
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
from datetime import date, datetime # noqa: F401
|
||||
|
||||
from typing import List, Dict # noqa: F401
|
||||
|
||||
from chromestatus_openapi.models.base_model import Model
|
||||
from chromestatus_openapi import util
|
||||
|
||||
|
||||
class FeatureLink(Model):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, id=None, name=None): # noqa: E501
|
||||
"""FeatureLink - a model defined in OpenAPI
|
||||
|
||||
:param id: The id of this FeatureLink. # noqa: E501
|
||||
:type id: int
|
||||
:param name: The name of this FeatureLink. # noqa: E501
|
||||
:type name: str
|
||||
"""
|
||||
self.openapi_types = {
|
||||
'id': int,
|
||||
'name': str
|
||||
}
|
||||
|
||||
self.attribute_map = {
|
||||
'id': 'id',
|
||||
'name': 'name'
|
||||
}
|
||||
|
||||
self._id = id
|
||||
self._name = name
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dikt) -> 'FeatureLink':
|
||||
"""Returns the dict as a model
|
||||
|
||||
:param dikt: A dict.
|
||||
:type: dict
|
||||
:return: The FeatureLink of this FeatureLink. # noqa: E501
|
||||
:rtype: FeatureLink
|
||||
"""
|
||||
return util.deserialize_model(dikt, cls)
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
"""Gets the id of this FeatureLink.
|
||||
|
||||
|
||||
:return: The id of this FeatureLink.
|
||||
:rtype: int
|
||||
"""
|
||||
return self._id
|
||||
|
||||
@id.setter
|
||||
def id(self, id: int):
|
||||
"""Sets the id of this FeatureLink.
|
||||
|
||||
|
||||
:param id: The id of this FeatureLink.
|
||||
:type id: int
|
||||
"""
|
||||
if id is None:
|
||||
raise ValueError("Invalid value for `id`, must not be `None`") # noqa: E501
|
||||
|
||||
self._id = id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Gets the name of this FeatureLink.
|
||||
|
||||
|
||||
:return: The name of this FeatureLink.
|
||||
:rtype: str
|
||||
"""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name: str):
|
||||
"""Sets the name of this FeatureLink.
|
||||
|
||||
|
||||
:param name: The name of this FeatureLink.
|
||||
:type name: str
|
||||
"""
|
||||
if name is None:
|
||||
raise ValueError("Invalid value for `name`, must not be `None`") # noqa: E501
|
||||
|
||||
self._name = name
|
|
@ -0,0 +1,93 @@
|
|||
from datetime import date, datetime # noqa: F401
|
||||
|
||||
from typing import List, Dict # noqa: F401
|
||||
|
||||
from chromestatus_openapi.models.base_model import Model
|
||||
from chromestatus_openapi.models.feature_link import FeatureLink
|
||||
from chromestatus_openapi import util
|
||||
|
||||
from chromestatus_openapi.models.feature_link import FeatureLink # noqa: E501
|
||||
|
||||
class SpecMentor(Model):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, email=None, mentored_features=None): # noqa: E501
|
||||
"""SpecMentor - a model defined in OpenAPI
|
||||
|
||||
:param email: The email of this SpecMentor. # noqa: E501
|
||||
:type email: str
|
||||
:param mentored_features: The mentored_features of this SpecMentor. # noqa: E501
|
||||
:type mentored_features: List[FeatureLink]
|
||||
"""
|
||||
self.openapi_types = {
|
||||
'email': str,
|
||||
'mentored_features': List[FeatureLink]
|
||||
}
|
||||
|
||||
self.attribute_map = {
|
||||
'email': 'email',
|
||||
'mentored_features': 'mentored_features'
|
||||
}
|
||||
|
||||
self._email = email
|
||||
self._mentored_features = mentored_features
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dikt) -> 'SpecMentor':
|
||||
"""Returns the dict as a model
|
||||
|
||||
:param dikt: A dict.
|
||||
:type: dict
|
||||
:return: The SpecMentor of this SpecMentor. # noqa: E501
|
||||
:rtype: SpecMentor
|
||||
"""
|
||||
return util.deserialize_model(dikt, cls)
|
||||
|
||||
@property
|
||||
def email(self) -> str:
|
||||
"""Gets the email of this SpecMentor.
|
||||
|
||||
|
||||
:return: The email of this SpecMentor.
|
||||
:rtype: str
|
||||
"""
|
||||
return self._email
|
||||
|
||||
@email.setter
|
||||
def email(self, email: str):
|
||||
"""Sets the email of this SpecMentor.
|
||||
|
||||
|
||||
:param email: The email of this SpecMentor.
|
||||
:type email: str
|
||||
"""
|
||||
if email is None:
|
||||
raise ValueError("Invalid value for `email`, must not be `None`") # noqa: E501
|
||||
|
||||
self._email = email
|
||||
|
||||
@property
|
||||
def mentored_features(self) -> List[FeatureLink]:
|
||||
"""Gets the mentored_features of this SpecMentor.
|
||||
|
||||
|
||||
:return: The mentored_features of this SpecMentor.
|
||||
:rtype: List[FeatureLink]
|
||||
"""
|
||||
return self._mentored_features
|
||||
|
||||
@mentored_features.setter
|
||||
def mentored_features(self, mentored_features: List[FeatureLink]):
|
||||
"""Sets the mentored_features of this SpecMentor.
|
||||
|
||||
|
||||
:param mentored_features: The mentored_features of this SpecMentor.
|
||||
:type mentored_features: List[FeatureLink]
|
||||
"""
|
||||
if mentored_features is None:
|
||||
raise ValueError("Invalid value for `mentored_features`, must not be `None`") # noqa: E501
|
||||
|
||||
self._mentored_features = mentored_features
|
|
@ -91,6 +91,32 @@ paths:
|
|||
- XsrfToken: []
|
||||
summary: List all components and possible users
|
||||
x-openapi-router-controller: chromestatus_openapi.controllers.default_controller
|
||||
/spec_mentors:
|
||||
get:
|
||||
operationId: list_spec_mentors
|
||||
parameters:
|
||||
- explode: true
|
||||
in: query
|
||||
name: after
|
||||
required: false
|
||||
schema:
|
||||
format: date
|
||||
type: string
|
||||
style: form
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpecMentor'
|
||||
type: array
|
||||
description: List of all the matching spec mentors.
|
||||
"400":
|
||||
description: The ?after query parameter isn't a valid date in ISO YYYY-MM-DD
|
||||
format.
|
||||
summary: List spec mentors and their activity
|
||||
x-openapi-router-controller: chromestatus_openapi.controllers.default_controller
|
||||
components:
|
||||
schemas:
|
||||
ComponentsUsersResponse:
|
||||
|
@ -195,6 +221,47 @@ components:
|
|||
title: owner
|
||||
type: boolean
|
||||
title: ComponentUsersRequest
|
||||
SpecMentor:
|
||||
example:
|
||||
mentored_features:
|
||||
- name: WebGPU
|
||||
id: 5703707724349440
|
||||
- name: WebGPU
|
||||
id: 5703707724349440
|
||||
email: email
|
||||
properties:
|
||||
email:
|
||||
format: email
|
||||
title: email
|
||||
type: string
|
||||
mentored_features:
|
||||
items:
|
||||
$ref: '#/components/schemas/FeatureLink'
|
||||
title: mentored_features
|
||||
type: array
|
||||
required:
|
||||
- email
|
||||
- mentored_features
|
||||
title: SpecMentor
|
||||
type: object
|
||||
FeatureLink:
|
||||
example:
|
||||
name: WebGPU
|
||||
id: 5703707724349440
|
||||
properties:
|
||||
id:
|
||||
example: 5703707724349440
|
||||
title: id
|
||||
type: integer
|
||||
name:
|
||||
example: WebGPU
|
||||
title: name
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
title: FeatureLink
|
||||
type: object
|
||||
securitySchemes:
|
||||
XsrfToken:
|
||||
in: header
|
||||
|
|
|
@ -4,6 +4,7 @@ from flask import json
|
|||
|
||||
from chromestatus_openapi.models.component_users_request import ComponentUsersRequest # noqa: E501
|
||||
from chromestatus_openapi.models.components_users_response import ComponentsUsersResponse # noqa: E501
|
||||
from chromestatus_openapi.models.spec_mentor import SpecMentor # noqa: E501
|
||||
from chromestatus_openapi.test import BaseTestCase
|
||||
|
||||
|
||||
|
@ -45,6 +46,23 @@ class TestDefaultController(BaseTestCase):
|
|||
self.assert200(response,
|
||||
'Response body is : ' + response.data.decode('utf-8'))
|
||||
|
||||
def test_list_spec_mentors(self):
|
||||
"""Test case for list_spec_mentors
|
||||
|
||||
List spec mentors and their activity
|
||||
"""
|
||||
query_string = [('after', '2013-10-20')]
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
response = self.client.open(
|
||||
'/api/v0/spec_mentors',
|
||||
method='GET',
|
||||
headers=headers,
|
||||
query_string=query_string)
|
||||
self.assert200(response,
|
||||
'Response body is : ' + response.data.decode('utf-8'))
|
||||
|
||||
def test_remove_user_from_component(self):
|
||||
"""Test case for remove_user_from_component
|
||||
|
||||
|
|
4
main.py
|
@ -34,6 +34,7 @@ from api import permissions_api
|
|||
from api import processes_api
|
||||
from api import reviews_api
|
||||
from api import settings_api
|
||||
from api import spec_mentors_api
|
||||
from api import stages_api
|
||||
from api import stars_api
|
||||
from api import token_refresh_api
|
||||
|
@ -140,6 +141,8 @@ api_routes: list[Route] = [
|
|||
Route(f'{API_BASE}/components/<int:component_id>/users/<int:user_id>',
|
||||
component_users.ComponentUsersAPI),
|
||||
|
||||
Route(f'{API_BASE}/spec_mentors', spec_mentors_api.SpecMentorsAPI),
|
||||
|
||||
Route(f'{API_BASE}/login', login_api.LoginAPI),
|
||||
Route(f'{API_BASE}/logout', logout_api.LogoutAPI),
|
||||
Route(f'{API_BASE}/currentuser/permissions', permissions_api.PermissionsAPI),
|
||||
|
@ -204,6 +207,7 @@ spa_page_routes = [
|
|||
Route('/metrics/feature/popularity'),
|
||||
Route('/metrics/feature/timeline/popularity'),
|
||||
Route('/metrics/feature/timeline/popularity/<int:bucket_id>'),
|
||||
Route('/reports/spec_mentors'),
|
||||
Route('/settings', defaults={'require_signin': True}),
|
||||
Route('/enterprise'),
|
||||
Route(
|
||||
|
|
|
@ -81,6 +81,28 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ComponentUsersRequest'
|
||||
/spec_mentors:
|
||||
get:
|
||||
summary: List spec mentors and their activity
|
||||
operationId: listSpecMentors
|
||||
parameters:
|
||||
- in: query
|
||||
name: after
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
responses:
|
||||
'200':
|
||||
description: >-
|
||||
List of all the matching spec mentors.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SpecMentor'
|
||||
'400':
|
||||
description: The ?after query parameter isn't a valid date in ISO YYYY-MM-DD format.
|
||||
components:
|
||||
securitySchemes:
|
||||
XsrfToken:
|
||||
|
@ -135,3 +157,28 @@ components:
|
|||
owner:
|
||||
type: boolean
|
||||
description: Impacts this user's ownership. For PUT, add ownership. For DELETE, remove ownership.
|
||||
SpecMentor:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
mentored_features:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FeatureLink'
|
||||
required:
|
||||
- email
|
||||
- mentored_features
|
||||
FeatureLink:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 5703707724349440
|
||||
name:
|
||||
type: string
|
||||
example: WebGPU
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"packages/playwright"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lit/task": "^1.0.0",
|
||||
"@polymer/iron-collapse": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
"@polymer/iron-iconset-svg": "^3.0.1",
|
||||
|
@ -60,7 +61,7 @@
|
|||
"minimist": ">=1.2.8",
|
||||
"path-parse": ">=1.0.7",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rollup": "^4.9.6",
|
||||
"rollup": "^4.10.0",
|
||||
"rollup-plugin-babel-minify": "^10.0.0",
|
||||
"sinon": "^17.0.1",
|
||||
"tar": ">=6.2.0",
|
||||
|
@ -74,7 +75,7 @@
|
|||
"gen/js/chromestatus-openapi": {
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3"
|
||||
"typescript": "^4.0"
|
||||
}
|
||||
},
|
||||
"gen/js/chromestatus-openapi/node_modules/typescript": {
|
||||
|
@ -2192,6 +2193,14 @@
|
|||
"@lit-labs/ssr-dom-shim": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lit/task": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/task/-/task-1.0.0.tgz",
|
||||
"integrity": "sha512-7jocGBh3yGlo3kKxQggZph2txK4X5GYNWp2FAsmV9u2spzUypwrzRzXe8I72icAb02B00+k2nlvxVcrQB6vyrw==",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.0.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
|
@ -2812,9 +2821,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
|
||||
"integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz",
|
||||
"integrity": "sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -2825,9 +2834,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
|
||||
"integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz",
|
||||
"integrity": "sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -2838,9 +2847,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
|
||||
"integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz",
|
||||
"integrity": "sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -2851,9 +2860,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
|
||||
"integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz",
|
||||
"integrity": "sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -2864,9 +2873,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
|
||||
"integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz",
|
||||
"integrity": "sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -2877,9 +2886,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
|
||||
"integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz",
|
||||
"integrity": "sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -2890,9 +2899,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
|
||||
"integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz",
|
||||
"integrity": "sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -2903,9 +2912,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
|
||||
"integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz",
|
||||
"integrity": "sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
@ -2916,9 +2925,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
|
||||
"integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz",
|
||||
"integrity": "sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -2929,9 +2938,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
|
||||
"integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz",
|
||||
"integrity": "sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -2942,9 +2951,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
|
||||
"integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz",
|
||||
"integrity": "sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -2955,9 +2964,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
|
||||
"integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz",
|
||||
"integrity": "sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -2968,9 +2977,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
|
||||
"integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz",
|
||||
"integrity": "sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -13262,9 +13271,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz",
|
||||
"integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.10.0.tgz",
|
||||
"integrity": "sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
|
@ -13277,19 +13286,19 @@
|
|||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.9.6",
|
||||
"@rollup/rollup-android-arm64": "4.9.6",
|
||||
"@rollup/rollup-darwin-arm64": "4.9.6",
|
||||
"@rollup/rollup-darwin-x64": "4.9.6",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.9.6",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.9.6",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.9.6",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.9.6",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.6",
|
||||
"@rollup/rollup-linux-x64-musl": "4.9.6",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.9.6",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.9.6",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.9.6",
|
||||
"@rollup/rollup-android-arm-eabi": "4.10.0",
|
||||
"@rollup/rollup-android-arm64": "4.10.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.10.0",
|
||||
"@rollup/rollup-darwin-x64": "4.10.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.10.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.10.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.10.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.10.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.10.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.10.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.10.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.10.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.10.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
@ -15619,7 +15628,7 @@
|
|||
"packages/playwright": {
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.41.1"
|
||||
"@playwright/test": "1.41.2"
|
||||
}
|
||||
},
|
||||
"packages/playwright/node_modules/@playwright/test": {
|
||||
|
@ -17142,6 +17151,14 @@
|
|||
"@lit-labs/ssr-dom-shim": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@lit/task": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/task/-/task-1.0.0.tgz",
|
||||
"integrity": "sha512-7jocGBh3yGlo3kKxQggZph2txK4X5GYNWp2FAsmV9u2spzUypwrzRzXe8I72icAb02B00+k2nlvxVcrQB6vyrw==",
|
||||
"requires": {
|
||||
"@lit/reactive-element": "^1.0.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
|
@ -17582,93 +17599,93 @@
|
|||
}
|
||||
},
|
||||
"@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
|
||||
"integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz",
|
||||
"integrity": "sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-android-arm64": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
|
||||
"integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz",
|
||||
"integrity": "sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
|
||||
"integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz",
|
||||
"integrity": "sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-darwin-x64": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
|
||||
"integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz",
|
||||
"integrity": "sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
|
||||
"integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz",
|
||||
"integrity": "sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
|
||||
"integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz",
|
||||
"integrity": "sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
|
||||
"integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz",
|
||||
"integrity": "sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
|
||||
"integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz",
|
||||
"integrity": "sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
|
||||
"integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz",
|
||||
"integrity": "sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
|
||||
"integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz",
|
||||
"integrity": "sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
|
||||
"integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz",
|
||||
"integrity": "sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
|
||||
"integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz",
|
||||
"integrity": "sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
|
||||
"integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz",
|
||||
"integrity": "sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
|
@ -19655,11 +19672,12 @@
|
|||
"chromestatus-openapi": {
|
||||
"version": "file:gen/js/chromestatus-openapi",
|
||||
"requires": {
|
||||
"typescript": "^5.3"
|
||||
"typescript": "^4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": {
|
||||
"version": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true
|
||||
}
|
||||
|
@ -25164,7 +25182,7 @@
|
|||
"playwright": {
|
||||
"version": "file:packages/playwright",
|
||||
"requires": {
|
||||
"@playwright/test": "1.41.1"
|
||||
"@playwright/test": "1.41.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
|
@ -25819,24 +25837,24 @@
|
|||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz",
|
||||
"integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.10.0.tgz",
|
||||
"integrity": "sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.9.6",
|
||||
"@rollup/rollup-android-arm64": "4.9.6",
|
||||
"@rollup/rollup-darwin-arm64": "4.9.6",
|
||||
"@rollup/rollup-darwin-x64": "4.9.6",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.9.6",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.9.6",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.9.6",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.9.6",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.6",
|
||||
"@rollup/rollup-linux-x64-musl": "4.9.6",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.9.6",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.9.6",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.9.6",
|
||||
"@rollup/rollup-android-arm-eabi": "4.10.0",
|
||||
"@rollup/rollup-android-arm64": "4.10.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.10.0",
|
||||
"@rollup/rollup-darwin-x64": "4.10.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.10.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.10.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.10.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.10.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.10.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.10.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.10.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.10.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.10.0",
|
||||
"@types/estree": "1.0.5",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
"minimist": ">=1.2.8",
|
||||
"path-parse": ">=1.0.7",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rollup": "^4.9.6",
|
||||
"rollup": "^4.10.0",
|
||||
"rollup-plugin-babel-minify": "^10.0.0",
|
||||
"sinon": "^17.0.1",
|
||||
"tar": ">=6.2.0",
|
||||
|
@ -102,6 +102,7 @@
|
|||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lit/task": "^1.0.0",
|
||||
"@polymer/iron-collapse": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
"@polymer/iron-iconset-svg": "^3.0.1",
|
||||
|
|
До Ширина: | Высота: | Размер: 85 KiB После Ширина: | Высота: | Размер: 89 KiB |
До Ширина: | Высота: | Размер: 96 KiB После Ширина: | Высота: | Размер: 99 KiB |
До Ширина: | Высота: | Размер: 111 KiB После Ширина: | Высота: | Размер: 114 KiB |
До Ширина: | Высота: | Размер: 116 KiB После Ширина: | Высота: | Размер: 119 KiB |
До Ширина: | Высота: | Размер: 79 KiB После Ширина: | Высота: | Размер: 83 KiB |
До Ширина: | Высота: | Размер: 88 KiB После Ширина: | Высота: | Размер: 90 KiB |
До Ширина: | Высота: | Размер: 136 KiB После Ширина: | Высота: | Размер: 140 KiB |
До Ширина: | Высота: | Размер: 140 KiB После Ширина: | Высота: | Размер: 142 KiB |
До Ширина: | Высота: | Размер: 117 KiB После Ширина: | Высота: | Размер: 121 KiB |
До Ширина: | Высота: | Размер: 121 KiB После Ширина: | Высота: | Размер: 123 KiB |
До Ширина: | Высота: | Размер: 128 KiB После Ширина: | Высота: | Размер: 131 KiB |
До Ширина: | Высота: | Размер: 136 KiB После Ширина: | Высота: | Размер: 139 KiB |
|
@ -0,0 +1,62 @@
|
|||
// @ts-check
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {captureConsoleMessages} from './test_utils';
|
||||
|
||||
import spec_mentor_api_result from './spec_mentor_api_result.json';
|
||||
|
||||
test.beforeEach(async ({page}, testInfo) => {
|
||||
captureConsoleMessages(page);
|
||||
});
|
||||
|
||||
test('spec mentors report renders', async ({page}) => {
|
||||
await page.route('/api/v0/spec_mentors?after=*', route => route.fulfill({
|
||||
status: 200,
|
||||
body: `)]}'\n${JSON.stringify(spec_mentor_api_result)}`,
|
||||
}));
|
||||
|
||||
await page.goto('/reports/spec_mentors');
|
||||
|
||||
// <details> elements have the 'group' role:
|
||||
await expect.soft(page.getByRole('group').filter({hasText: /has mentored:/}))
|
||||
.toHaveText([/expert@example.com/, /mentor@example.org/]);
|
||||
|
||||
const expertLink = page.locator('sl-details', {hasText: 'expert@example.com has mentored'})
|
||||
.getByRole('link');
|
||||
await expect.soft(expertLink).toHaveText("First feature");
|
||||
await expect.soft(expertLink).toHaveAttribute("href", /\/feature\/9009/);
|
||||
|
||||
const mentorLinks = page.locator('sl-details', {hasText: "mentor@example.org has mentored"})
|
||||
.getByRole('link');
|
||||
await expect.soft(mentorLinks.nth(0)).toHaveText("Second feature");
|
||||
await expect.soft(mentorLinks.nth(0)).toHaveAttribute("href", /\/feature\/9010/);
|
||||
await expect.soft(mentorLinks.nth(1)).toHaveText("First feature");
|
||||
await expect.soft(mentorLinks.nth(1)).toHaveAttribute("href", /\/feature\/9009/);
|
||||
});
|
||||
|
||||
test('after query param affects api request', async ({page}) => {
|
||||
await page.route('/api/v0/spec_mentors?after=*', route => route.fulfill({
|
||||
status: 200,
|
||||
body: `)]}'\n${JSON.stringify(spec_mentor_api_result)}`,
|
||||
}));
|
||||
|
||||
const apiRequest = page.waitForRequest(/\/api\/v0\/spec_mentors\?after=2023-04-05$/);
|
||||
await page.goto('/reports/spec_mentors?after=2023-04-05');
|
||||
await apiRequest;
|
||||
|
||||
// Should be reflected in the form field.
|
||||
await expect(page.getByLabel('updated after')).toHaveValue('2023-04-05');
|
||||
});
|
||||
|
||||
test('after form field affects api request and page url', async ({page}) => {
|
||||
await page.route('/api/v0/spec_mentors?after=*', route => route.fulfill({
|
||||
status: 200,
|
||||
body: `)]}'\n${JSON.stringify(spec_mentor_api_result)}`,
|
||||
}));
|
||||
|
||||
await page.goto('/reports/spec_mentors');
|
||||
|
||||
const apiRequest = page.waitForRequest(/\/api\/v0\/spec_mentors\?after=2023-05-06$/);
|
||||
await page.getByLabel('updated after').fill('2023-05-06');
|
||||
await apiRequest;
|
||||
await expect(page).toHaveURL('/reports/spec_mentors?after=2023-05-06')
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"email": "expert@example.com",
|
||||
"mentored_features": [
|
||||
{"id": 9009, "name": "First feature"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"email": "mentor@example.org",
|
||||
"mentored_features": [
|
||||
{"id": 9010, "name": "Second feature"},
|
||||
{"id": 9009, "name": "First feature"}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -21,12 +21,13 @@ datastore.
|
|||
This script can also set a user as admin, to facilitate testing those parts of the site.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urljoin
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from google.cloud import ndb # type: ignore
|
||||
|
@ -39,48 +40,199 @@ os.environ.setdefault('GOOGLE_CLOUD_PROJECT', 'cr-status-staging')
|
|||
os.environ.setdefault('SERVER_SOFTWARE', 'gunicorn')
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
# ruff: noqa: E402
|
||||
from internals.core_enums import MISC
|
||||
from internals.core_models import FeatureEntry
|
||||
from internals.core_models import FeatureEntry, MilestoneSet, Stage
|
||||
from internals.user_models import AppUser
|
||||
from pages.guide import FeatureCreateHandler
|
||||
|
||||
|
||||
def add_features(server: str, after: datetime):
|
||||
"""Fetches features from `server` and inserts them into the dev datastore."""
|
||||
target = urljoin(server, 'features.json')
|
||||
logging.info('Fetching features from %s.', target)
|
||||
features = requests.get(target, timeout=120)
|
||||
features.raise_for_status()
|
||||
def add_features(server: str, after: datetime, detailsAfter: datetime):
|
||||
"""Fetches features from `server` and inserts them into the dev datastore."""
|
||||
target = urljoin(server, 'features.json')
|
||||
logging.info('Fetching features from %s.', target)
|
||||
features = requests.get(target, timeout=120)
|
||||
features.raise_for_status()
|
||||
|
||||
parsed_features = features.json()
|
||||
recent_features = [
|
||||
f for f in parsed_features
|
||||
if after < datetime.fromisoformat(f['updated']['when'])
|
||||
]
|
||||
logging.info('Adding %d recent features of %d total to the datastore.',
|
||||
len(recent_features), len(parsed_features))
|
||||
parsed_features = features.json()
|
||||
recent_features = [
|
||||
f for f in parsed_features
|
||||
if after < datetime.fromisoformat(f['updated']['when'])
|
||||
]
|
||||
logging.info('Adding %d recent features of %d total to the datastore.',
|
||||
len(recent_features), len(parsed_features))
|
||||
|
||||
for f in recent_features:
|
||||
fe = FeatureEntry(id=f['id'],
|
||||
created=datetime.fromisoformat(f['created']['when']),
|
||||
updated=datetime.fromisoformat(f['updated']['when']))
|
||||
fe.name = f['name']
|
||||
fe.summary = f['summary']
|
||||
fe.category = f.get('category_int', MISC)
|
||||
# fe.feature_type = f['feature_type'] # Not available from the dump?
|
||||
fe.impl_status_chrome = f['browsers']['chrome']['status']['val']
|
||||
fe.standard_maturity = f['standards']['maturity']['val']
|
||||
fe.ff_views = f['browsers']['ff']['view']['val']
|
||||
fe.safari_views = f['browsers']['safari']['view']['val']
|
||||
fe.web_dev_views = f['browsers']['webdev']['view']['val']
|
||||
fe.put()
|
||||
for f in recent_features:
|
||||
fe = FeatureEntry(id=f['id'],
|
||||
created=datetime.fromisoformat(f['created']['when']),
|
||||
updated=datetime.fromisoformat(f['updated']['when']))
|
||||
fe.name = f['name']
|
||||
fe.summary = f['summary']
|
||||
fe.category = MISC
|
||||
fe.breaking_change = f['breaking_change']
|
||||
fe.first_enterprise_notification_milestone = f[
|
||||
'first_enterprise_notification_milestone']
|
||||
fe.blink_components = f['blink_components']
|
||||
fe.sample_links = f['resources']['samples']
|
||||
fe.doc_links = f['resources']['docs']
|
||||
fe.creator_email = f['created']['by']
|
||||
fe.updater_email = f['updated']['by']
|
||||
|
||||
fe.impl_status_chrome = f['browsers']['chrome']['status']['val']
|
||||
fe.bug_url = f['browsers']['chrome']['bug']
|
||||
fe.devrel_emails = f['browsers']['chrome']['devrel']
|
||||
fe.owner_emails = f['browsers']['chrome']['owners']
|
||||
fe.prefixed = f['browsers']['chrome']['prefixed']
|
||||
fe.spec_link = f['standards']['spec']
|
||||
fe.standard_maturity = f['standards']['maturity']['val']
|
||||
fe.ff_views = f['browsers']['ff']['view']['val']
|
||||
fe.ff_views_link = f['browsers']['ff']['view']['url']
|
||||
fe.ff_views_notes = f['browsers']['ff']['view']['notes']
|
||||
fe.safari_views = f['browsers']['safari']['view']['val']
|
||||
fe.safari_views_link = f['browsers']['safari']['view']['url']
|
||||
fe.safari_views_notes = f['browsers']['safari']['view']['notes']
|
||||
fe.web_dev_views = f['browsers']['webdev']['view']['val']
|
||||
fe.web_dev_views_link = f['browsers']['webdev']['view']['url']
|
||||
fe.web_dev_views_notes = f['browsers']['webdev']['view']['notes']
|
||||
fe.other_views_notes = f['browsers']['other']['view']['notes']
|
||||
|
||||
details = None
|
||||
if fe.updated > detailsAfter:
|
||||
url = urljoin(server, f'api/v0/features/{f["id"]}')
|
||||
result = requests.get(url)
|
||||
if 400 <= result.status_code:
|
||||
logging.error('Could not fetch details for %r', url)
|
||||
continue
|
||||
result.raise_for_status()
|
||||
details = json.loads(result.text[5:])
|
||||
|
||||
fe.star_count = details['star_count']
|
||||
fe.search_tags = details['search_tags']
|
||||
fe.category = details['category_int']
|
||||
fe.feature_notes = details['feature_notes']
|
||||
fe.enterprise_feature_categories = details[
|
||||
'enterprise_feature_categories']
|
||||
if details['accurate_as_of'] is not None:
|
||||
fe.accurate_as_of = datetime.fromisoformat(details['accurate_as_of'])
|
||||
fe.editor_emails = details['editor_emails']
|
||||
fe.cc_emails = details['cc_emails']
|
||||
fe.spec_mentor_emails = details['spec_mentor_emails']
|
||||
fe.deleted = details['deleted']
|
||||
fe.feature_type = details['feature_type_int']
|
||||
fe.intent_stage = details['intent_stage_int']
|
||||
fe.active_stage_id = details['active_stage_id']
|
||||
fe.bug_url = details['bug_url']
|
||||
fe.launch_bug_url = details['launch_bug_url']
|
||||
fe.screenshot_links = details['screenshot_links']
|
||||
fe.first_enterprise_notification_milestone = details[
|
||||
'first_enterprise_notification_milestone']
|
||||
fe.flag_name = details['flag_name']
|
||||
fe.finch_name = details['finch_name']
|
||||
fe.non_finch_justification = details['non_finch_justification']
|
||||
fe.ongoing_constraints = details['ongoing_constraints']
|
||||
fe.motivation = details['motivation']
|
||||
fe.devtrial_instructions = details['devtrial_instructions']
|
||||
fe.activation_risks = details['activation_risks']
|
||||
fe.measurement = details['measurement']
|
||||
fe.availability_expectation = details['availability_expectation']
|
||||
fe.adoption_expectation = details['adoption_expectation']
|
||||
fe.adoption_plan = details['adoption_plan']
|
||||
fe.initial_public_proposal_url = details['initial_public_proposal_url']
|
||||
fe.explainer_links = details['explainer_links']
|
||||
fe.requires_embedder_support = details['requires_embedder_support']
|
||||
fe.api_spec = details['api_spec']
|
||||
fe.interop_compat_risks = details['interop_compat_risks']
|
||||
fe.all_platforms = details['all_platforms']
|
||||
fe.all_platforms_descr = details['all_platforms_descr']
|
||||
fe.non_oss_deps = details['non_oss_deps']
|
||||
fe.anticipated_spec_changes = details['anticipated_spec_changes']
|
||||
fe.security_risks = details['security_risks']
|
||||
fe.ergonomics_risks = details['ergonomics_risks']
|
||||
fe.wpt = details['wpt']
|
||||
fe.wpt_descr = details['wpt_descr']
|
||||
fe.webview_risks = details['webview_risks']
|
||||
fe.devrel_emails = details['devrel_emails']
|
||||
fe.debuggability = details['debuggability']
|
||||
fe.doc_links = details['doc_links']
|
||||
fe.sample_links = details['sample_links']
|
||||
fe.search_tags = details['tags']
|
||||
fe.tag_review = details['tag_review']
|
||||
fe.tag_review_status = details['tag_review_status_int']
|
||||
fe.security_review_status = details['security_review_status_int']
|
||||
fe.privacy_review_status = details['privacy_review_status_int']
|
||||
|
||||
fe.experiment_timeline = details['experiment_timeline']
|
||||
|
||||
for stage in details['stages']:
|
||||
s = Stage(id=stage['id'],
|
||||
feature_id=stage['feature_id'],
|
||||
stage_type=stage['stage_type'])
|
||||
s.created = datetime.fromisoformat(stage['created'])
|
||||
s.ot_description = stage['ot_description']
|
||||
s.display_name = stage['display_name']
|
||||
s.pm_emails = stage['pm_emails']
|
||||
s.tl_emails = stage['tl_emails']
|
||||
s.ux_emails = stage['ux_emails']
|
||||
s.te_emails = stage['te_emails']
|
||||
s.intent_thread_url = stage['intent_thread_url']
|
||||
|
||||
s.announcement_url = stage['announcement_url']
|
||||
s.experiment_goals = stage['experiment_goals']
|
||||
s.experiment_risks = stage['experiment_risks']
|
||||
s.origin_trial_id = stage['origin_trial_id']
|
||||
s.origin_trial_feedback_url = stage['origin_trial_feedback_url']
|
||||
s.ot_action_requested = stage['ot_action_requested']
|
||||
s.ot_approval_buganizer_component = stage[
|
||||
'ot_approval_buganizer_component']
|
||||
s.ot_approval_criteria_url = stage['ot_approval_criteria_url']
|
||||
s.ot_approval_group_email = stage['ot_approval_group_email']
|
||||
s.ot_chromium_trial_name = stage['ot_chromium_trial_name']
|
||||
s.ot_description = stage['ot_description']
|
||||
s.ot_display_name = stage['ot_display_name']
|
||||
s.ot_documentation_url = stage['ot_documentation_url']
|
||||
s.ot_emails = stage['ot_emails']
|
||||
s.ot_feedback_submission_url = stage['ot_feedback_submission_url']
|
||||
s.ot_has_third_party_support = stage['ot_has_third_party_support']
|
||||
s.ot_is_critical_trial = stage['ot_is_critical_trial']
|
||||
s.ot_is_deprecation_trial = stage['ot_is_deprecation_trial']
|
||||
s.ot_owner_email = stage['ot_owner_email']
|
||||
s.ot_require_approvals = stage['ot_require_approvals']
|
||||
s.ot_webfeature_use_counter = stage['ot_webfeature_use_counter']
|
||||
s.experiment_extension_reason = stage['experiment_extension_reason']
|
||||
s.ot_stage_id = stage['ot_stage_id']
|
||||
s.finch_url = stage['finch_url']
|
||||
|
||||
s.rollout_milestone = stage['rollout_milestone']
|
||||
s.rollout_platforms = stage['rollout_platforms']
|
||||
s.rollout_details = stage['rollout_details']
|
||||
s.rollout_impact = stage['rollout_impact']
|
||||
s.enterprise_policies = stage['enterprise_policies']
|
||||
|
||||
s.milestones = MilestoneSet()
|
||||
s.milestones.desktop_first = stage['desktop_first']
|
||||
s.milestones.android_first = stage['android_first']
|
||||
s.milestones.ios_first = stage['ios_first']
|
||||
s.milestones.webview_first = stage['webview_first']
|
||||
s.milestones.desktop_last = stage['desktop_last']
|
||||
s.milestones.android_last = stage['android_last']
|
||||
s.milestones.ios_last = stage['ios_last']
|
||||
s.milestones.webview_last = stage['webview_last']
|
||||
|
||||
s.put()
|
||||
|
||||
fe.put()
|
||||
|
||||
if details is None:
|
||||
FeatureCreateHandler().write_gates_and_stages_for_feature(
|
||||
fe.key.id(), fe.feature_type)
|
||||
|
||||
|
||||
def add_admin(email: str):
|
||||
"""Makes the user named `email` an admin."""
|
||||
user = AppUser()
|
||||
user.email = email
|
||||
user.is_admin = True
|
||||
user.put()
|
||||
"""Makes the user named `email` an admin."""
|
||||
user = AppUser()
|
||||
user.email = email
|
||||
user.is_admin = True
|
||||
user.put()
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='seed the development datastore')
|
||||
|
@ -93,15 +245,24 @@ parser.add_argument('--admin',
|
|||
parser.add_argument(
|
||||
'--after',
|
||||
type=datetime.fromisoformat,
|
||||
help='only add features that have been updated since AFTER (in ISO date format)',
|
||||
help=
|
||||
'only add features that have been updated since AFTER (in ISO date format)',
|
||||
default='0001-01-01')
|
||||
parser.add_argument(
|
||||
'--details-after',
|
||||
type=datetime.fromisoformat,
|
||||
help=
|
||||
'fetch more details for each feature that was updated since DETAILS-AFTER',
|
||||
default='9999-01-01')
|
||||
|
||||
args = parser.parse_args()
|
||||
client = ndb.Client()
|
||||
with client.context():
|
||||
if args.server:
|
||||
add_features(server=args.server, after=args.after)
|
||||
if args.server:
|
||||
add_features(server=args.server,
|
||||
after=args.after,
|
||||
detailsAfter=args.details_after)
|
||||
|
||||
if args.admin:
|
||||
logging.info('Making %s an admin.', args.admin)
|
||||
add_admin(args.admin)
|
||||
if args.admin:
|
||||
logging.info('Making %s an admin.', args.admin)
|
||||
add_admin(args.admin)
|
||||
|
|