Merge remote-tracking branch 'origin/main' into dlaliberte-intentpreview-1030

This commit is contained in:
Daniel LaLiberte 2024-02-15 22:44:15 +00:00
Родитель fc33724ff4 f26e2ec82e
Коммит cfbbb3c7d2
40 изменённых файлов: 1470 добавлений и 163 удалений

63
api/spec_mentors_api.py Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -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

250
package-lock.json сгенерированный
Просмотреть файл

@ -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)