Remove the rest of old schema references (#2910)
* Remove the rest of old schema references * Keep maintenance scripts * move script Route
This commit is contained in:
Родитель
ece3f7f471
Коммит
ad9386cdb9
|
@ -157,7 +157,6 @@ export class ChromedashAdminBlinkPage extends LitElement {
|
|||
html`<div id="component-count">listing ${this.components.length} components</div>`
|
||||
}
|
||||
</div>
|
||||
<a href="/admin/subscribers" class="view_owners_linke">List by owner & their features →</a>
|
||||
</div>
|
||||
<div class="layout horizontal subheader_toggles">
|
||||
<!-- <paper-toggle-button> doesn't working here. Related links:
|
||||
|
|
|
@ -1,232 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2023 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.
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from google.cloud import ndb # type: ignore
|
||||
|
||||
from api.legacy_converters import feature_to_legacy_json
|
||||
from framework import rediscache
|
||||
from framework import users
|
||||
from internals.core_enums import *
|
||||
from internals.legacy_models import Feature
|
||||
from internals.feature_helpers import filter_unlisted, _new_crbug_url
|
||||
|
||||
|
||||
def get_by_ids_legacy(feature_ids: list[int],
|
||||
update_cache: bool=False) -> list[dict[str, Any]]:
|
||||
"""Return a list of JSON dicts for the specified features.
|
||||
|
||||
Because the cache may rarely have stale data, this should only be
|
||||
used for displaying data read-only, not for populating forms or
|
||||
procesing a POST to edit data. For editing use case, load the
|
||||
data from NDB directly.
|
||||
"""
|
||||
result_dict = {}
|
||||
futures = []
|
||||
|
||||
for feature_id in feature_ids:
|
||||
lookup_key = Feature.feature_cache_key(
|
||||
Feature.DEFAULT_CACHE_KEY, feature_id)
|
||||
feature = rediscache.get(lookup_key)
|
||||
if feature is None or update_cache:
|
||||
futures.append(Feature.get_by_id_async(feature_id))
|
||||
else:
|
||||
result_dict[feature_id] = feature
|
||||
|
||||
for future in futures:
|
||||
unformatted_feature: Optional[Feature] = future.get_result()
|
||||
if unformatted_feature and not unformatted_feature.deleted:
|
||||
feature = feature_to_legacy_json(unformatted_feature)
|
||||
feature['updated_display'] = (
|
||||
unformatted_feature.updated.strftime("%Y-%m-%d"))
|
||||
feature['new_crbug_url'] = _new_crbug_url(
|
||||
unformatted_feature.blink_components, unformatted_feature.bug_url,
|
||||
unformatted_feature.impl_status_chrome, unformatted_feature.owner)
|
||||
store_key = Feature.feature_cache_key(
|
||||
Feature.DEFAULT_CACHE_KEY, unformatted_feature.key.integer_id())
|
||||
rediscache.set(store_key, feature)
|
||||
result_dict[unformatted_feature.key.integer_id()] = feature
|
||||
|
||||
result_list = [
|
||||
result_dict[feature_id] for feature_id in feature_ids
|
||||
if feature_id in result_dict]
|
||||
return result_list
|
||||
|
||||
|
||||
def get_all_legacy(limit=None, order='-updated', filterby=None,
|
||||
update_cache=False, keys_only=False):
|
||||
"""Return JSON dicts for entities that fit the filterby criteria.
|
||||
|
||||
Because the cache may rarely have stale data, this should only be
|
||||
used for displaying data read-only, not for populating forms or
|
||||
procesing a POST to edit data. For editing use case, load the
|
||||
data from NDB directly.
|
||||
"""
|
||||
KEY = '%s|%s|%s|%s' % (Feature.DEFAULT_CACHE_KEY, order, limit, keys_only)
|
||||
|
||||
# TODO(ericbidelman): Support more than one filter.
|
||||
if filterby is not None:
|
||||
s = ('%s%s' % (filterby[0], filterby[1])).replace(' ', '')
|
||||
KEY += '|%s' % s
|
||||
|
||||
feature_list = rediscache.get(KEY)
|
||||
|
||||
if feature_list is None or update_cache:
|
||||
query = Feature.query().order(-Feature.updated) #.order('name')
|
||||
query = query.filter(Feature.deleted == False)
|
||||
|
||||
# TODO(ericbidelman): Support more than one filter.
|
||||
if filterby:
|
||||
filter_type, comparator = filterby
|
||||
if filter_type == 'can_edit':
|
||||
# can_edit will check if the user has any access to edit the feature.
|
||||
# This includes being an owner, editor, or the original creator
|
||||
# of the feature.
|
||||
query = query.filter(
|
||||
ndb.OR(Feature.owner == comparator, Feature.editors == comparator,
|
||||
Feature.creator == comparator))
|
||||
else:
|
||||
query = query.filter(getattr(Feature, filter_type) == comparator)
|
||||
|
||||
feature_list = query.fetch(limit, keys_only=keys_only)
|
||||
if not keys_only:
|
||||
feature_list = [
|
||||
feature_to_legacy_json(f) for f in feature_list]
|
||||
|
||||
rediscache.set(KEY, feature_list)
|
||||
|
||||
return feature_list
|
||||
|
||||
|
||||
def filter_unlisted_legacy(feature_list: list[dict]) -> list[dict]:
|
||||
"""Filters a feature list to display only features the user should see."""
|
||||
user = users.get_current_user()
|
||||
email = None
|
||||
if user:
|
||||
email = user.email()
|
||||
listed_features = []
|
||||
for f in feature_list:
|
||||
# Owners and editors of a feature should still be able to see their features.
|
||||
if ((not f.get('unlisted', False)) or
|
||||
('browsers' in f and email in f['browsers']['chrome']['owners']) or
|
||||
(email in f.get('editors', [])) or
|
||||
(email is not None and f.get('creator') == email)):
|
||||
listed_features.append(f)
|
||||
|
||||
return listed_features
|
||||
|
||||
|
||||
def get_chronological_legacy(limit=None, update_cache: bool=False,
|
||||
show_unlisted: bool=False) -> list[dict]:
|
||||
"""Return a list of JSON dicts for features, ordered by milestone.
|
||||
|
||||
Because the cache may rarely have stale data, this should only be
|
||||
used for displaying data read-only, not for populating forms or
|
||||
procesing a POST to edit data. For editing use case, load the
|
||||
data from NDB directly.
|
||||
"""
|
||||
cache_key = '%s|%s|%s' % (Feature.DEFAULT_CACHE_KEY,
|
||||
'cronorder', limit)
|
||||
|
||||
feature_list = rediscache.get(cache_key)
|
||||
logging.info('getting chronological feature list')
|
||||
|
||||
# On cache miss, do a db query.
|
||||
if not feature_list or update_cache:
|
||||
logging.info('recomputing chronological feature list')
|
||||
# Features that are in-dev or proposed.
|
||||
q = Feature.query()
|
||||
q = q.order(Feature.impl_status_chrome)
|
||||
q = q.order(Feature.name)
|
||||
q = q.filter(Feature.impl_status_chrome.IN(
|
||||
(PROPOSED, IN_DEVELOPMENT)))
|
||||
pre_release_future = q.fetch_async(None)
|
||||
|
||||
# Shipping features. Exclude features that do not have a desktop
|
||||
# shipping milestone.
|
||||
q = Feature.query()
|
||||
q = q.order(-Feature.shipped_milestone)
|
||||
q = q.order(Feature.name)
|
||||
q = q.filter(Feature.shipped_milestone != None)
|
||||
shipping_features_future = q.fetch_async(None)
|
||||
|
||||
# Features with an android shipping milestone but no desktop milestone.
|
||||
q = Feature.query()
|
||||
q = q.order(-Feature.shipped_android_milestone)
|
||||
q = q.order(Feature.name)
|
||||
q = q.filter(Feature.shipped_milestone == None)
|
||||
android_only_shipping_features_future = q.fetch_async(None)
|
||||
|
||||
# Features with no active development.
|
||||
q = Feature.query()
|
||||
q = q.order(Feature.name)
|
||||
q = q.filter(Feature.impl_status_chrome == NO_ACTIVE_DEV)
|
||||
no_active_future = q.fetch_async(None)
|
||||
|
||||
# No longer pursuing features.
|
||||
q = Feature.query()
|
||||
q = q.order(Feature.name)
|
||||
q = q.filter(Feature.impl_status_chrome == NO_LONGER_PURSUING)
|
||||
no_longer_pursuing_features_future = q.fetch_async(None)
|
||||
|
||||
logging.info('Waiting on futures')
|
||||
pre_release = pre_release_future.result()
|
||||
android_only_shipping_features = (
|
||||
android_only_shipping_features_future.result())
|
||||
no_active = no_active_future.result()
|
||||
no_longer_pursuing_features = no_longer_pursuing_features_future.result()
|
||||
logging.info('Waiting on shipping_features_future')
|
||||
shipping_features = shipping_features_future.result()
|
||||
|
||||
shipping_features.extend(android_only_shipping_features)
|
||||
|
||||
shipping_features = [
|
||||
f for f in shipping_features
|
||||
if (IN_DEVELOPMENT < f.impl_status_chrome
|
||||
< NO_LONGER_PURSUING)]
|
||||
|
||||
def getSortingMilestone(feature):
|
||||
feature._sort_by_milestone = (feature.shipped_milestone or
|
||||
feature.shipped_android_milestone or
|
||||
0)
|
||||
return feature
|
||||
|
||||
# Sort the feature list on either Android shipping milestone or desktop
|
||||
# shipping milestone, depending on which is specified. If a desktop
|
||||
# milestone is defined, that will take default.
|
||||
shipping_features = list(map(getSortingMilestone, shipping_features))
|
||||
|
||||
# First sort by name, then sort by feature milestone (latest first).
|
||||
shipping_features.sort(key=lambda f: f.name, reverse=False)
|
||||
shipping_features.sort(key=lambda f: f._sort_by_milestone, reverse=True)
|
||||
|
||||
# Constructor the proper ordering.
|
||||
all_features = []
|
||||
all_features.extend(pre_release)
|
||||
all_features.extend(shipping_features)
|
||||
all_features.extend(no_active)
|
||||
all_features.extend(no_longer_pursuing_features)
|
||||
all_features = [f for f in all_features if not f.deleted]
|
||||
|
||||
feature_list = [feature_to_legacy_json(f) for f in all_features]
|
||||
|
||||
Feature._annotate_first_of_milestones(feature_list)
|
||||
|
||||
rediscache.set(cache_key, feature_list)
|
||||
|
||||
if not show_unlisted:
|
||||
feature_list = filter_unlisted_legacy(feature_list)
|
||||
return feature_list
|
|
@ -1,83 +0,0 @@
|
|||
|
||||
# Copyright 2023 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.
|
||||
|
||||
import testing_config # Must be imported before the module under test.
|
||||
|
||||
from internals.core_enums import *
|
||||
from internals import legacy_helpers
|
||||
from internals.legacy_models import Feature
|
||||
|
||||
|
||||
class LegacyHelpersTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Legacy entities for testing legacy functions.
|
||||
self.legacy_feature_2 = Feature(
|
||||
name='feature b', summary='sum',
|
||||
owner=['feature_owner@example.com'], category=1,
|
||||
creator="someuser@example.com")
|
||||
self.legacy_feature_2.put()
|
||||
|
||||
self.legacy_feature_1 = Feature(
|
||||
name='feature a', summary='sum', impl_status_chrome=3,
|
||||
owner=['feature_owner@example.com'], category=1)
|
||||
self.legacy_feature_1.put()
|
||||
|
||||
self.legacy_feature_4 = Feature(
|
||||
name='feature d', summary='sum', category=1, impl_status_chrome=2,
|
||||
owner=['feature_owner@example.com'])
|
||||
self.legacy_feature_4.put()
|
||||
|
||||
self.legacy_feature_3 = Feature(
|
||||
name='feature c', summary='sum', category=1, impl_status_chrome=2,
|
||||
owner=['feature_owner@example.com'])
|
||||
self.legacy_feature_3.put()
|
||||
|
||||
def tearDown(self):
|
||||
for kind in [Feature]:
|
||||
for entity in kind.query():
|
||||
entity.key.delete()
|
||||
|
||||
def test_get_chronological__normal(self):
|
||||
"""We can retrieve a list of features."""
|
||||
actual = legacy_helpers.get_chronological_legacy()
|
||||
names = [f['name'] for f in actual]
|
||||
self.assertEqual(
|
||||
['feature c', 'feature d', 'feature a', 'feature b'],
|
||||
names)
|
||||
self.assertEqual(True, actual[0]['first_of_milestone'])
|
||||
self.assertEqual(False, hasattr(actual[1], 'first_of_milestone'))
|
||||
self.assertEqual(True, actual[2]['first_of_milestone'])
|
||||
self.assertEqual(False, hasattr(actual[3], 'first_of_milestone'))
|
||||
|
||||
def test_get_chronological__unlisted(self):
|
||||
"""Unlisted features are not included in the list."""
|
||||
self.legacy_feature_2.unlisted = True
|
||||
self.legacy_feature_2.put()
|
||||
actual = legacy_helpers.get_chronological_legacy(update_cache=True)
|
||||
names = [f['name'] for f in actual]
|
||||
self.assertEqual(
|
||||
['feature c', 'feature d', 'feature a'],
|
||||
names)
|
||||
|
||||
def test_get_chronological__unlisted_shown(self):
|
||||
"""Unlisted features are included for users with edit access."""
|
||||
self.legacy_feature_2.unlisted = True
|
||||
self.legacy_feature_2.put()
|
||||
actual = legacy_helpers.get_chronological_legacy(update_cache=True, show_unlisted=True)
|
||||
names = [f['name'] for f in actual]
|
||||
self.assertEqual(
|
||||
['feature c', 'feature d', 'feature a', 'feature b'],
|
||||
names)
|
|
@ -1,450 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2023 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.
|
||||
|
||||
# Import needed to reference a class within its own class method.
|
||||
# https://stackoverflow.com/a/33533514
|
||||
from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from google.cloud import ndb # type: ignore
|
||||
|
||||
from framework import rediscache
|
||||
from internals.core_enums import *
|
||||
from internals import fetchchannels
|
||||
import settings
|
||||
|
||||
|
||||
##################################
|
||||
### Legacy core models ###
|
||||
##################################
|
||||
SIMPLE_TYPES = (int, float, bool, dict, str, list)
|
||||
|
||||
class DictModel(ndb.Model):
|
||||
# def to_dict(self):
|
||||
# return dict([(p, str(getattr(self, p))) for p in self.properties()])
|
||||
|
||||
def is_saved(self):
|
||||
if self.key:
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_dict(self):
|
||||
output = {}
|
||||
|
||||
for key, prop in self._properties.items():
|
||||
# Skip obsolete values that are still in our datastore
|
||||
if not hasattr(self, key):
|
||||
continue
|
||||
|
||||
value = getattr(self, key)
|
||||
|
||||
if value is None or isinstance(value, SIMPLE_TYPES):
|
||||
output[key] = value
|
||||
elif isinstance(value, datetime.date):
|
||||
# Convert date/datetime to ms-since-epoch ("new Date()").
|
||||
#ms = time.mktime(value.utctimetuple())
|
||||
#ms += getattr(value, 'microseconds', 0) / 1000
|
||||
#output[key] = int(ms)
|
||||
output[key] = str(value)
|
||||
elif isinstance(value, ndb.GeoPt):
|
||||
output[key] = {'lat': value.lat, 'lon': value.lon}
|
||||
elif isinstance(value, ndb.Model):
|
||||
output[key] = value.to_dict()
|
||||
elif isinstance(value, ndb.model.User):
|
||||
output[key] = value.email()
|
||||
else:
|
||||
raise ValueError('cannot encode ' + repr(prop))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class Feature(DictModel):
|
||||
"""Container for a feature."""
|
||||
|
||||
DEFAULT_CACHE_KEY = 'features'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Initialise Feature.blink_components with a default value. If
|
||||
# name is present in kwargs then it would mean constructor is
|
||||
# being called for creating a new feature rather than for fetching
|
||||
# an existing feature.
|
||||
if 'name' in kwargs:
|
||||
if 'blink_components' not in kwargs:
|
||||
kwargs['blink_components'] = [settings.DEFAULT_COMPONENT]
|
||||
|
||||
super(Feature, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def feature_cache_key(cls, cache_key, feature_id):
|
||||
return '%s|%s' % (cache_key, feature_id)
|
||||
|
||||
@classmethod
|
||||
def feature_cache_prefix(cls):
|
||||
return '%s|*' % (Feature.DEFAULT_CACHE_KEY)
|
||||
|
||||
@classmethod
|
||||
def _first_of_milestone_v2(self, feature_list, milestone, start=0):
|
||||
for i in range(start, len(feature_list)):
|
||||
f = feature_list[i]
|
||||
desktop_milestone = f['browsers']['chrome'].get('desktop', None)
|
||||
android_milestone = f['browsers']['chrome'].get('android', None)
|
||||
status = f['browsers']['chrome']['status'].get('text', None)
|
||||
|
||||
if (str(desktop_milestone) == str(milestone) or status == str(milestone)):
|
||||
return i
|
||||
elif (desktop_milestone == None and
|
||||
str(android_milestone) == str(milestone)):
|
||||
return i
|
||||
|
||||
return -1
|
||||
|
||||
@classmethod
|
||||
def _annotate_first_of_milestones(self, feature_list):
|
||||
try:
|
||||
omaha_data = fetchchannels.get_omaha_data()
|
||||
|
||||
win_versions = omaha_data[0]['versions']
|
||||
|
||||
# Find the latest canary major version from the list of windows versions.
|
||||
canary_versions = [
|
||||
x for x in win_versions
|
||||
if x.get('channel') and x.get('channel').startswith('canary')]
|
||||
LATEST_VERSION = int(canary_versions[0].get('version').split('.')[0])
|
||||
|
||||
milestones = list(range(1, LATEST_VERSION + 1))
|
||||
milestones.reverse()
|
||||
versions = [
|
||||
IMPLEMENTATION_STATUS[PROPOSED],
|
||||
IMPLEMENTATION_STATUS[IN_DEVELOPMENT],
|
||||
IMPLEMENTATION_STATUS[DEPRECATED],
|
||||
]
|
||||
versions.extend(milestones)
|
||||
versions.append(IMPLEMENTATION_STATUS[NO_ACTIVE_DEV])
|
||||
versions.append(IMPLEMENTATION_STATUS[NO_LONGER_PURSUING])
|
||||
|
||||
last_good_idx = 0
|
||||
for i, ver in enumerate(versions):
|
||||
idx = Feature._first_of_milestone_v2(
|
||||
feature_list, ver, start=last_good_idx)
|
||||
if idx != -1:
|
||||
feature_list[idx]['first_of_milestone'] = True
|
||||
last_good_idx = idx
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
|
||||
def put(self, **kwargs) -> Any:
|
||||
key = super(Feature, self).put(**kwargs)
|
||||
# Invalidate rediscache for the individual feature view.
|
||||
cache_key = Feature.feature_cache_key(
|
||||
Feature.DEFAULT_CACHE_KEY, self.key.integer_id())
|
||||
rediscache.delete(cache_key)
|
||||
|
||||
return key
|
||||
|
||||
# Metadata.
|
||||
created = ndb.DateTimeProperty(auto_now_add=True)
|
||||
updated = ndb.DateTimeProperty(auto_now=True)
|
||||
accurate_as_of = ndb.DateTimeProperty(auto_now=False)
|
||||
updated_by = ndb.UserProperty()
|
||||
created_by = ndb.UserProperty()
|
||||
|
||||
# General info.
|
||||
category = ndb.IntegerProperty(required=True)
|
||||
creator = ndb.StringProperty()
|
||||
name = ndb.StringProperty(required=True)
|
||||
feature_type = ndb.IntegerProperty(default=FEATURE_TYPE_INCUBATE_ID)
|
||||
intent_stage = ndb.IntegerProperty(default=INTENT_NONE)
|
||||
summary = ndb.StringProperty(required=True)
|
||||
unlisted = ndb.BooleanProperty(default=False)
|
||||
enterprise_feature_categories = ndb.StringProperty(repeated=True)
|
||||
# TODO(jrobbins): Add an entry_state enum to track app-specific lifecycle
|
||||
# info for a feature entry as distinct from process-specific stage.
|
||||
deleted = ndb.BooleanProperty(default=False)
|
||||
motivation = ndb.StringProperty()
|
||||
star_count = ndb.IntegerProperty(default=0)
|
||||
search_tags = ndb.StringProperty(repeated=True)
|
||||
comments = ndb.StringProperty()
|
||||
owner = ndb.StringProperty(repeated=True)
|
||||
editors = ndb.StringProperty(repeated=True)
|
||||
cc_recipients = ndb.StringProperty(repeated=True)
|
||||
footprint = ndb.IntegerProperty() # Deprecated
|
||||
breaking_change = ndb.BooleanProperty(default=False)
|
||||
|
||||
# Tracability to intent discussion threads
|
||||
intent_to_implement_url = ndb.StringProperty()
|
||||
intent_to_implement_subject_line = ndb.StringProperty()
|
||||
intent_to_ship_url = ndb.StringProperty()
|
||||
intent_to_ship_subject_line = ndb.StringProperty()
|
||||
ready_for_trial_url = ndb.StringProperty()
|
||||
intent_to_experiment_url = ndb.StringProperty()
|
||||
intent_to_experiment_subject_line = ndb.StringProperty()
|
||||
intent_to_extend_experiment_url = ndb.StringProperty()
|
||||
intent_to_extend_experiment_subject_line = ndb.StringProperty()
|
||||
# Currently, only one is needed.
|
||||
i2e_lgtms = ndb.StringProperty(repeated=True)
|
||||
i2s_lgtms = ndb.StringProperty(repeated=True)
|
||||
|
||||
# Chromium details.
|
||||
bug_url = ndb.StringProperty()
|
||||
launch_bug_url = ndb.StringProperty()
|
||||
initial_public_proposal_url = ndb.StringProperty()
|
||||
blink_components = ndb.StringProperty(repeated=True)
|
||||
devrel = ndb.StringProperty(repeated=True)
|
||||
|
||||
impl_status_chrome = ndb.IntegerProperty(required=True, default=NO_ACTIVE_DEV)
|
||||
shipped_milestone = ndb.IntegerProperty()
|
||||
shipped_android_milestone = ndb.IntegerProperty()
|
||||
shipped_ios_milestone = ndb.IntegerProperty()
|
||||
shipped_webview_milestone = ndb.IntegerProperty()
|
||||
requires_embedder_support = ndb.BooleanProperty(default=False)
|
||||
|
||||
# DevTrial details.
|
||||
devtrial_instructions = ndb.StringProperty()
|
||||
flag_name = ndb.StringProperty()
|
||||
interop_compat_risks = ndb.StringProperty()
|
||||
ergonomics_risks = ndb.StringProperty()
|
||||
activation_risks = ndb.StringProperty()
|
||||
security_risks = ndb.StringProperty()
|
||||
webview_risks = ndb.StringProperty()
|
||||
debuggability = ndb.StringProperty()
|
||||
all_platforms = ndb.BooleanProperty()
|
||||
all_platforms_descr = ndb.StringProperty()
|
||||
wpt = ndb.BooleanProperty()
|
||||
wpt_descr = ndb.StringProperty()
|
||||
dt_milestone_desktop_start = ndb.IntegerProperty()
|
||||
dt_milestone_android_start = ndb.IntegerProperty()
|
||||
dt_milestone_ios_start = ndb.IntegerProperty()
|
||||
# Webview DT is currently not offered in the UI because there is no way
|
||||
# to set flags.
|
||||
dt_milestone_webview_start = ndb.IntegerProperty()
|
||||
# Note: There are no dt end milestones because a dev trail implicitly
|
||||
# ends when the feature ships or is abandoned.
|
||||
|
||||
visibility = ndb.IntegerProperty(required=False, default=1) # Deprecated
|
||||
|
||||
# Standards details.
|
||||
standardization = ndb.IntegerProperty(required=True,
|
||||
default=EDITORS_DRAFT) # Deprecated
|
||||
standard_maturity = ndb.IntegerProperty(required=True, default=UNSET_STD)
|
||||
spec_link = ndb.StringProperty()
|
||||
api_spec = ndb.BooleanProperty(default=False)
|
||||
spec_mentors = ndb.StringProperty(repeated=True)
|
||||
|
||||
security_review_status = ndb.IntegerProperty(default=REVIEW_PENDING)
|
||||
privacy_review_status = ndb.IntegerProperty(default=REVIEW_PENDING)
|
||||
|
||||
tag_review = ndb.StringProperty()
|
||||
tag_review_status = ndb.IntegerProperty(default=REVIEW_PENDING)
|
||||
|
||||
prefixed = ndb.BooleanProperty()
|
||||
|
||||
explainer_links = ndb.StringProperty(repeated=True)
|
||||
|
||||
ff_views = ndb.IntegerProperty(required=True, default=NO_PUBLIC_SIGNALS)
|
||||
# Deprecated
|
||||
ie_views = ndb.IntegerProperty(required=True, default=NO_PUBLIC_SIGNALS)
|
||||
safari_views = ndb.IntegerProperty(required=True, default=NO_PUBLIC_SIGNALS)
|
||||
web_dev_views = ndb.IntegerProperty(required=True, default=DEV_NO_SIGNALS)
|
||||
|
||||
ff_views_link = ndb.StringProperty()
|
||||
ie_views_link = ndb.StringProperty() # Deprecated
|
||||
safari_views_link = ndb.StringProperty()
|
||||
web_dev_views_link = ndb.StringProperty()
|
||||
|
||||
ff_views_notes = ndb.StringProperty()
|
||||
ie_views_notes = ndb.StringProperty() # Deprecated
|
||||
safari_views_notes = ndb.StringProperty()
|
||||
web_dev_views_notes = ndb.StringProperty()
|
||||
other_views_notes = ndb.StringProperty()
|
||||
|
||||
doc_links = ndb.StringProperty(repeated=True)
|
||||
measurement = ndb.StringProperty()
|
||||
availability_expectation = ndb.TextProperty()
|
||||
adoption_expectation = ndb.TextProperty()
|
||||
adoption_plan = ndb.TextProperty()
|
||||
sample_links = ndb.StringProperty(repeated=True)
|
||||
non_oss_deps = ndb.StringProperty()
|
||||
|
||||
experiment_goals = ndb.StringProperty()
|
||||
experiment_timeline = ndb.StringProperty()
|
||||
ot_milestone_desktop_start = ndb.IntegerProperty()
|
||||
ot_milestone_desktop_end = ndb.IntegerProperty()
|
||||
ot_milestone_android_start = ndb.IntegerProperty()
|
||||
ot_milestone_android_end = ndb.IntegerProperty()
|
||||
ot_milestone_webview_start = ndb.IntegerProperty()
|
||||
ot_milestone_webview_end = ndb.IntegerProperty()
|
||||
experiment_risks = ndb.StringProperty()
|
||||
experiment_extension_reason = ndb.StringProperty()
|
||||
ongoing_constraints = ndb.StringProperty()
|
||||
origin_trial_feedback_url = ndb.StringProperty()
|
||||
anticipated_spec_changes = ndb.StringProperty()
|
||||
|
||||
finch_url = ndb.StringProperty()
|
||||
|
||||
# Flag set to avoid migrating data that has already been migrated.
|
||||
stages_migrated = ndb.BooleanProperty(default=False)
|
||||
|
||||
|
||||
####################################
|
||||
### Legacy review models ###
|
||||
####################################
|
||||
class Approval(ndb.Model):
|
||||
"""Describes the current state of one approval on a feature."""
|
||||
|
||||
# Not used: PREPARING = 0
|
||||
NA = 1
|
||||
REVIEW_REQUESTED = 2
|
||||
REVIEW_STARTED = 3
|
||||
NEEDS_WORK = 4
|
||||
APPROVED = 5
|
||||
DENIED = 6
|
||||
NO_RESPONSE = 7
|
||||
INTERNAL_REVIEW = 8
|
||||
APPROVAL_VALUES = {
|
||||
# Not used: PREPARING: 'preparing',
|
||||
NA: 'na',
|
||||
REVIEW_REQUESTED: 'review_requested',
|
||||
REVIEW_STARTED: 'review_started',
|
||||
NEEDS_WORK: 'needs_work',
|
||||
APPROVED: 'approved',
|
||||
DENIED: 'denied',
|
||||
NO_RESPONSE: 'no_response',
|
||||
INTERNAL_REVIEW: 'internal_review',
|
||||
}
|
||||
|
||||
FINAL_STATES = [NA, APPROVED, DENIED]
|
||||
|
||||
feature_id = ndb.IntegerProperty(required=True)
|
||||
field_id = ndb.IntegerProperty(required=True)
|
||||
state = ndb.IntegerProperty(required=True)
|
||||
set_on = ndb.DateTimeProperty(required=True)
|
||||
set_by = ndb.StringProperty(required=True)
|
||||
|
||||
@classmethod
|
||||
def get_approvals(
|
||||
cls, feature_id=None, field_id=None, states=None, set_by=None,
|
||||
limit=None) -> list[Approval]:
|
||||
"""Return the requested approvals."""
|
||||
query = Approval.query().order(Approval.set_on)
|
||||
if feature_id is not None:
|
||||
query = query.filter(Approval.feature_id == feature_id)
|
||||
if field_id is not None:
|
||||
query = query.filter(Approval.field_id == field_id)
|
||||
if states is not None:
|
||||
query = query.filter(Approval.state.IN(states))
|
||||
if set_by is not None:
|
||||
query = query.filter(Approval.set_by == set_by)
|
||||
# Query with STRONG consistency because ndb defaults to
|
||||
# EVENTUAL consistency and we run this query immediately after
|
||||
# saving the user's change that we want included in the query.
|
||||
approvals = query.fetch(limit, read_consistency=ndb.STRONG)
|
||||
return approvals
|
||||
|
||||
@classmethod
|
||||
def is_valid_state(cls, new_state):
|
||||
"""Return true if new_state is valid."""
|
||||
return new_state in cls.APPROVAL_VALUES
|
||||
|
||||
@classmethod
|
||||
def set_approval(cls, feature_id, field_id, new_state, set_by_email):
|
||||
"""Add or update an approval value."""
|
||||
if not cls.is_valid_state(new_state):
|
||||
raise ValueError('Invalid approval state')
|
||||
|
||||
now = datetime.datetime.now()
|
||||
existing_list = cls.get_approvals(
|
||||
feature_id=feature_id, field_id=field_id, set_by=set_by_email)
|
||||
if existing_list:
|
||||
existing = existing_list[0]
|
||||
existing.set_on = now
|
||||
existing.state = new_state
|
||||
existing.put()
|
||||
logging.info('existing approval is %r', existing.key.integer_id())
|
||||
return
|
||||
|
||||
new_appr = Approval(
|
||||
feature_id=feature_id, field_id=field_id, state=new_state,
|
||||
set_on=now, set_by=set_by_email)
|
||||
new_appr.put()
|
||||
logging.info('new_appr is %r', new_appr.key.integer_id())
|
||||
|
||||
@classmethod
|
||||
def clear_request(cls, feature_id, field_id):
|
||||
"""After the review requirement has been satisfied, remove the request."""
|
||||
review_requests = cls.get_approvals(
|
||||
feature_id=feature_id, field_id=field_id, states=[cls.REVIEW_REQUESTED])
|
||||
for rr in review_requests:
|
||||
rr.key.delete()
|
||||
|
||||
# Note: We keep REVIEW_REQUEST Vote entities.
|
||||
|
||||
|
||||
class ApprovalConfig(ndb.Model):
|
||||
"""Allows customization of an approval field for one feature."""
|
||||
|
||||
feature_id = ndb.IntegerProperty(required=True)
|
||||
field_id = ndb.IntegerProperty(required=True)
|
||||
owners = ndb.StringProperty(repeated=True)
|
||||
next_action = ndb.DateProperty()
|
||||
additional_review = ndb.BooleanProperty(default=False)
|
||||
|
||||
@classmethod
|
||||
def get_configs(cls, feature_id):
|
||||
"""Return approval configs for all approval fields."""
|
||||
query = ApprovalConfig.query(ApprovalConfig.feature_id == feature_id)
|
||||
configs = query.fetch(None)
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def set_config(
|
||||
cls, feature_id, field_id, owners, next_action, additional_review):
|
||||
"""Add or update an approval config object."""
|
||||
config = ApprovalConfig(feature_id=feature_id, field_id=field_id)
|
||||
for existing in cls.get_configs(feature_id):
|
||||
if existing.field_id == field_id:
|
||||
config = existing
|
||||
|
||||
config.owners = owners or []
|
||||
config.next_action = next_action
|
||||
config.additional_review = additional_review
|
||||
config.put()
|
||||
|
||||
|
||||
class Comment(ndb.Model):
|
||||
"""A review comment on a feature."""
|
||||
feature_id = ndb.IntegerProperty(required=True)
|
||||
field_id = ndb.IntegerProperty() # The approval field_id, or general comment.
|
||||
created = ndb.DateTimeProperty(auto_now_add=True)
|
||||
author = ndb.StringProperty()
|
||||
content = ndb.StringProperty()
|
||||
deleted_by = ndb.StringProperty()
|
||||
migrated = ndb.BooleanProperty()
|
||||
# If the user set an approval value, we capture that here so that we can
|
||||
# display a change log. This could be generalized to a list of separate
|
||||
# Amendment entities, but that complexity is not needed yet.
|
||||
old_approval_state = ndb.IntegerProperty()
|
||||
new_approval_state = ndb.IntegerProperty()
|
||||
|
||||
@classmethod
|
||||
def get_comments(cls, feature_id, field_id=None):
|
||||
"""Return review comments for an approval."""
|
||||
query = Comment.query().order(Comment.created)
|
||||
query = query.filter(Comment.feature_id == feature_id)
|
||||
if field_id:
|
||||
query = query.filter(Comment.field_id == field_id)
|
||||
comments = query.fetch(None)
|
||||
return comments
|
|
@ -1,105 +0,0 @@
|
|||
# Copyright 2023 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.
|
||||
|
||||
import testing_config # Must be imported before the module under test.
|
||||
|
||||
import datetime
|
||||
|
||||
from internals.legacy_models import Approval
|
||||
from internals.core_models import FeatureEntry
|
||||
|
||||
|
||||
class ApprovalTest(testing_config.CustomTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.feature_1 = FeatureEntry(
|
||||
name='feature a', summary='sum', category=1, impl_status_chrome=3)
|
||||
self.feature_1.put()
|
||||
self.feature_1_id = self.feature_1.key.integer_id()
|
||||
self.appr_1 = Approval(
|
||||
feature_id=self.feature_1_id, field_id=1,
|
||||
state=Approval.REVIEW_REQUESTED,
|
||||
set_on=datetime.datetime.now() - datetime.timedelta(1),
|
||||
set_by='one@example.com')
|
||||
self.appr_1.put()
|
||||
|
||||
self.appr_2 = Approval(
|
||||
feature_id=self.feature_1_id, field_id=1,
|
||||
state=Approval.APPROVED,
|
||||
set_on=datetime.datetime.now(),
|
||||
set_by='two@example.com')
|
||||
self.appr_2.put()
|
||||
self.appr_3 = Approval(
|
||||
feature_id=self.feature_1_id, field_id=1,
|
||||
state=Approval.APPROVED,
|
||||
set_on=datetime.datetime.now() + datetime.timedelta(1),
|
||||
set_by='three@example.com')
|
||||
self.appr_3.put()
|
||||
|
||||
def tearDown(self):
|
||||
self.feature_1.key.delete()
|
||||
for appr in Approval.query().fetch():
|
||||
appr.key.delete()
|
||||
|
||||
def test_get_approvals(self):
|
||||
"""We can retrieve Approval entities."""
|
||||
actual = Approval.get_approvals(feature_id=self.feature_1_id)
|
||||
self.assertEqual(3, len(actual))
|
||||
self.assertEqual(Approval.REVIEW_REQUESTED, actual[0].state)
|
||||
self.assertEqual(Approval.APPROVED, actual[1].state)
|
||||
self.assertEqual(
|
||||
sorted(actual, key=lambda appr: appr.set_on),
|
||||
actual)
|
||||
|
||||
actual = Approval.get_approvals(field_id=1)
|
||||
self.assertEqual(Approval.REVIEW_REQUESTED, actual[0].state)
|
||||
self.assertEqual(Approval.APPROVED, actual[1].state)
|
||||
|
||||
actual = Approval.get_approvals(
|
||||
states={Approval.REVIEW_REQUESTED,
|
||||
Approval.REVIEW_STARTED})
|
||||
self.assertEqual(1, len(actual))
|
||||
|
||||
actual = Approval.get_approvals(set_by='one@example.com')
|
||||
self.assertEqual(1, len(actual))
|
||||
self.assertEqual(Approval.REVIEW_REQUESTED, actual[0].state)
|
||||
|
||||
def test_is_valid_state(self):
|
||||
"""We know what approval states are valid."""
|
||||
self.assertTrue(
|
||||
Approval.is_valid_state(Approval.REVIEW_REQUESTED))
|
||||
self.assertFalse(Approval.is_valid_state(None))
|
||||
self.assertFalse(Approval.is_valid_state('not an int'))
|
||||
self.assertFalse(Approval.is_valid_state(999))
|
||||
|
||||
def test_set_approval(self):
|
||||
"""We can set an Approval entity."""
|
||||
Approval.set_approval(
|
||||
self.feature_1_id, 2, Approval.REVIEW_REQUESTED,
|
||||
'owner@example.com')
|
||||
self.assertEqual(
|
||||
4,
|
||||
len(Approval.query().fetch(None)))
|
||||
|
||||
def test_clear_request(self):
|
||||
"""We can clear a review request so that it is no longer pending."""
|
||||
self.appr_1.state = Approval.REVIEW_REQUESTED
|
||||
self.appr_1.put()
|
||||
|
||||
Approval.clear_request(self.feature_1_id, 1)
|
||||
|
||||
remaining_apprs = Approval.get_approvals(
|
||||
feature_id=self.feature_1_id, field_id=1,
|
||||
states=[Approval.REVIEW_REQUESTED])
|
||||
self.assertEqual([], remaining_apprs)
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright 2022 Google Inc.
|
||||
# Copyright 2023 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License")
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -18,8 +18,7 @@ from google.cloud import ndb # type: ignore
|
|||
from framework.basehandlers import FlaskHandler
|
||||
from internals import approval_defs
|
||||
from internals.core_models import FeatureEntry, Stage
|
||||
from internals.legacy_models import Feature
|
||||
from internals.review_models import Gate, Vote
|
||||
from internals.review_models import Gate
|
||||
from internals.core_enums import *
|
||||
|
||||
|
||||
|
@ -103,73 +102,3 @@ class WriteMissingGates(FlaskHandler):
|
|||
ndb.put_multi(gates_to_write)
|
||||
|
||||
return f'{len(gates_to_write)} missing gates created for stages.'
|
||||
|
||||
|
||||
class MigrateLGTMFields(FlaskHandler):
|
||||
|
||||
def get_template_data(self, **kwargs) -> str:
|
||||
"""Migrates old Feature subject lgtms to their respective votes."""
|
||||
self.require_cron_header()
|
||||
|
||||
count = 0
|
||||
votes_to_create = []
|
||||
vote_dict = self.get_vote_dict()
|
||||
features = Feature.query()
|
||||
for f in features:
|
||||
if not f.i2e_lgtms:
|
||||
continue
|
||||
for email in f.i2e_lgtms:
|
||||
if self.has_existing_vote(email, GATE_API_ORIGIN_TRIAL, f, vote_dict):
|
||||
continue
|
||||
|
||||
# i2e_lgtms (Intent to Experiment)'s gate_type is GATE_API_ORIGIN_TRIAL.
|
||||
votes_to_create.append(self.create_new_vote(
|
||||
email, GATE_API_ORIGIN_TRIAL, f))
|
||||
count += 1
|
||||
|
||||
for f in features:
|
||||
if not f.i2s_lgtms:
|
||||
continue
|
||||
for email in f.i2s_lgtms:
|
||||
if self.has_existing_vote(email, GATE_API_SHIP, f, vote_dict):
|
||||
continue
|
||||
|
||||
# i2s_lgtms (Intent to Ship)'s gate_type is GATE_API_SHIP.
|
||||
votes_to_create.append(self.create_new_vote(
|
||||
email, GATE_API_SHIP, f))
|
||||
count += 1
|
||||
|
||||
# Only create 100 votes at a time.
|
||||
votes_to_create = votes_to_create[:100]
|
||||
for new_vote in votes_to_create:
|
||||
approval_defs.set_vote(new_vote.feature_id, new_vote.gate_type,
|
||||
new_vote.state, new_vote.set_by)
|
||||
return f'{len(votes_to_create)} of {count} lgtms fields migrated.'
|
||||
|
||||
def get_vote_dict(self):
|
||||
vote_dict = {}
|
||||
votes = Vote.query().fetch(None)
|
||||
for vote in votes:
|
||||
if vote.feature_id in vote_dict:
|
||||
vote_dict[vote.feature_id].append(vote)
|
||||
else:
|
||||
vote_dict[vote.feature_id] = [vote]
|
||||
return vote_dict
|
||||
|
||||
def has_existing_vote(self, email, gate_type, f, vote_dict):
|
||||
f_id = f.key.integer_id()
|
||||
if f_id not in vote_dict:
|
||||
return False
|
||||
|
||||
for v in vote_dict[f_id]:
|
||||
# Check if set by the same reviewer and the same gate_type.
|
||||
if v.set_by == email and v.gate_type == gate_type:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def create_new_vote(self, email, gate_type, f):
|
||||
f_id = f.key.integer_id()
|
||||
vote = Vote(feature_id=f_id, gate_type=gate_type,
|
||||
state=Vote.APPROVED, set_by=email)
|
||||
return vote
|
15
main.py
15
main.py
|
@ -39,13 +39,12 @@ from framework import csp
|
|||
from framework import sendemail
|
||||
from internals import detect_intent
|
||||
from internals import fetchmetrics
|
||||
from internals import maintenance_scripts
|
||||
from internals import notifier
|
||||
from internals import data_backup
|
||||
from internals import inactive_users
|
||||
from internals import search_fulltext
|
||||
from internals import schema_migration
|
||||
from internals import reminders
|
||||
from pages import blink_handler
|
||||
from pages import featurelist
|
||||
from pages import guide
|
||||
from pages import intentpreview
|
||||
|
@ -191,7 +190,6 @@ spa_page_routes = [
|
|||
]
|
||||
|
||||
mpa_page_routes: list[Route] = [
|
||||
Route('/admin/subscribers', blink_handler.SubscribersHandler),
|
||||
Route('/admin/users/new', users.UserListHandler),
|
||||
|
||||
Route('/admin/features/launch/<int:feature_id>',
|
||||
|
@ -234,12 +232,11 @@ internals_routes: list[Route] = [
|
|||
Route('/tasks/email-reviewers', notifier.FeatureReviewHandler),
|
||||
Route('/tasks/email-comments', notifier.FeatureCommentHandler),
|
||||
|
||||
Route('/admin/schema_migration_gate_status',
|
||||
schema_migration.EvaluateGateStatus),
|
||||
Route('/admin/schema_migration_missing_gates',
|
||||
schema_migration.WriteMissingGates),
|
||||
Route('/admin/schema_migration_lgtm_fields',
|
||||
schema_migration.MigrateLGTMFields),
|
||||
# Maintenance scripts.
|
||||
Route('/scripts/evaluate_gate_status',
|
||||
maintenance_scripts.EvaluateGateStatus),
|
||||
Route('/scripts/write_missing_gates',
|
||||
maintenance_scripts.WriteMissingGates),
|
||||
]
|
||||
|
||||
dev_routes: list[Route] = []
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 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.
|
||||
|
||||
import collections
|
||||
|
||||
from framework import basehandlers
|
||||
from framework import permissions
|
||||
from internals import legacy_helpers
|
||||
from internals import user_models
|
||||
from api.channels_api import construct_chrome_channels_details
|
||||
|
||||
class SubscribersHandler(basehandlers.FlaskHandler):
|
||||
|
||||
TEMPLATE_PATH = 'admin/subscribers.html'
|
||||
|
||||
@permissions.require_admin_site
|
||||
def get_template_data(self, **kwargs):
|
||||
users = user_models.FeatureOwner.query().order(
|
||||
user_models.FeatureOwner.name).fetch(None)
|
||||
feature_list = legacy_helpers.get_chronological_legacy()
|
||||
|
||||
milestone = self.get_int_arg('milestone')
|
||||
if milestone is not None:
|
||||
feature_list = [
|
||||
f for f in feature_list
|
||||
if (f['shipped_milestone'] or
|
||||
f['shipped_android_milestone']) == milestone]
|
||||
|
||||
list_features_per_owner = 'showFeatures' in self.request.args
|
||||
for user in users:
|
||||
# user.subscribed_components = [key.get() for key in user.blink_components]
|
||||
user.owned_components = [
|
||||
key.get() for key in user.primary_blink_components]
|
||||
for component in user.owned_components:
|
||||
component.features = []
|
||||
if list_features_per_owner:
|
||||
component.features = [
|
||||
f for f in feature_list
|
||||
if component.name in f['blink_components']]
|
||||
|
||||
details = construct_chrome_channels_details()
|
||||
|
||||
template_data = {
|
||||
'subscribers': users,
|
||||
'channels': collections.OrderedDict([
|
||||
('stable', details['stable']),
|
||||
('beta', details['beta']),
|
||||
('dev', details['dev']),
|
||||
('canary', details['canary']),
|
||||
]),
|
||||
'selected_milestone': milestone
|
||||
}
|
||||
return template_data
|
|
@ -1,103 +0,0 @@
|
|||
# Copyright 2022 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.
|
||||
|
||||
import testing_config # Must be imported before the module under test.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import flask
|
||||
import werkzeug
|
||||
import html5lib
|
||||
import settings
|
||||
|
||||
from google.cloud import ndb # type: ignore
|
||||
from pages import blink_handler
|
||||
from internals import user_models
|
||||
|
||||
test_app = flask.Flask(__name__,
|
||||
template_folder=settings.get_flask_template_path())
|
||||
|
||||
# Load testdata to be used across all of the CustomTestCases
|
||||
TESTDATA = testing_config.Testdata(__file__)
|
||||
|
||||
class SubscribersTemplateTest(testing_config.CustomTestCase):
|
||||
|
||||
HANDLER_CLASS = blink_handler.SubscribersHandler
|
||||
|
||||
def setUp(self):
|
||||
# need to patch in setup because the details are retreived here.
|
||||
# unable to use method decorator for setUp.
|
||||
self.mock_chrome_details_patch = mock.patch(
|
||||
'pages.blink_handler.construct_chrome_channels_details')
|
||||
mock_chrome_details = self.mock_chrome_details_patch.start()
|
||||
mock_chrome_details.return_value = {
|
||||
"stable": {"mstone": 1},
|
||||
"beta": {"mstone": 2},
|
||||
"dev": {"mstone": 3},
|
||||
"canary": {"mstone": 4},
|
||||
}
|
||||
|
||||
self.request_path = self.HANDLER_CLASS.TEMPLATE_PATH
|
||||
self.handler = self.HANDLER_CLASS()
|
||||
|
||||
self.app_admin = user_models.AppUser(email='admin@example.com')
|
||||
self.app_admin.is_admin = True
|
||||
self.app_admin.put()
|
||||
|
||||
testing_config.sign_in('admin@example.com', 123567890)
|
||||
|
||||
self.component_1 = user_models.BlinkComponent(name='Blink')
|
||||
self.component_1.put()
|
||||
self.component_2 = user_models.BlinkComponent(name='Blink>Accessibility')
|
||||
self.component_2.put()
|
||||
self.component_owner_1 = user_models.FeatureOwner(
|
||||
name='owner_1', email='owner_1@example.com',
|
||||
primary_blink_components=[self.component_1.key, self.component_2.key])
|
||||
self.component_owner_1.key = ndb.Key('FeatureOwner', 111)
|
||||
self.component_owner_1.put()
|
||||
self.watcher_1 = user_models.FeatureOwner(
|
||||
name='watcher_1', email='watcher_1@example.com',
|
||||
watching_all_features=True)
|
||||
self.watcher_1.key = ndb.Key('FeatureOwner', 222)
|
||||
self.watcher_1.put()
|
||||
|
||||
with test_app.test_request_context(self.request_path):
|
||||
self.template_data = self.handler.get_template_data()
|
||||
self.template_data.update(self.handler.get_common_data())
|
||||
self.template_data['nonce'] = 'fake nonce'
|
||||
self.template_data['xsrf_token'] = ''
|
||||
self.template_data['xsrf_token_expires'] = 0
|
||||
self.full_template_path = self.handler.get_template_path(self.template_data)
|
||||
|
||||
self.maxDiff = None
|
||||
|
||||
def tearDown(self):
|
||||
self.mock_chrome_details_patch.stop()
|
||||
self.watcher_1.key.delete()
|
||||
self.component_owner_1.key.delete()
|
||||
self.component_1.key.delete()
|
||||
self.component_2.key.delete()
|
||||
testing_config.sign_out()
|
||||
self.app_admin.key.delete()
|
||||
|
||||
def test_html_rendering(self):
|
||||
"""We can render the template with valid html."""
|
||||
with test_app.app_context():
|
||||
template_text = self.handler.render(
|
||||
self.template_data, self.full_template_path)
|
||||
parser = html5lib.HTMLParser(strict=True)
|
||||
document = parser.parse(template_text)
|
||||
# TESTDATA.make_golden(template_text, 'SubscribersTemplateTest_test_html_rendering.html')
|
||||
self.assertMultiLineEqual(
|
||||
TESTDATA['SubscribersTemplateTest_test_html_rendering.html'], template_text)
|
|
@ -1,225 +0,0 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
|
||||
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.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Local testing</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
|
||||
<meta name="theme-color" content="#366597">
|
||||
|
||||
<link rel="stylesheet" href="/static/css/base.css?v=Undeployed" />
|
||||
|
||||
<link rel="icon" sizes="192x192" href="/static/img/crstatus_192.png">
|
||||
|
||||
<!-- iOS: run in full-screen mode and display upper status bar as translucent -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<link rel="apple-touch-icon" href="/static/img/crstatus_128.png">
|
||||
<link rel="apple-touch-icon-precomposed" href="/static/img/crstatus_128.png">
|
||||
<link rel="shortcut icon" href="/static/img/crstatus_128.png">
|
||||
|
||||
<link rel="preconnect" href="https://www.google-analytics.com" crossorigin>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/static/css/main.css?v=Undeployed">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/static/css/subscribers.css?v=Undeployed">
|
||||
|
||||
|
||||
|
||||
<script src="https://accounts.google.com/gsi/client" async defer nonce="fake nonce"></script>
|
||||
|
||||
<script nonce="fake nonce">
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get("loginStatus") == 'False') {
|
||||
alert('Please log in.');
|
||||
}
|
||||
</script>
|
||||
<script nonce="fake nonce" src="/static/js/metric.min.js?v=Undeployed"></script>
|
||||
<script nonce="fake nonce" src="/static/js/cs-client.min.js?v=Undeployed"></script>
|
||||
|
||||
|
||||
<script nonce="fake nonce">
|
||||
window.csClient = new ChromeStatusClient(
|
||||
'', 0);
|
||||
</script>
|
||||
|
||||
<script type="module" nonce="fake nonce" defer
|
||||
src="/static/dist/components.js?v=Undeployed"></script>
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body class="loading" data-path="/admin/subscribers.html?">
|
||||
|
||||
<div id="app-content-container">
|
||||
<div>
|
||||
<div class="main-toolbar">
|
||||
<div class="toolbar-content">
|
||||
<chromedash-header
|
||||
appTitle="Local testing"
|
||||
currentPage="/admin/subscribers.html?"
|
||||
googleSignInClientId="914217904764-enfcea61q4hqe7ak8kkuteglrbhk8el1.apps.googleusercontent.com">
|
||||
</chromedash-header>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div id="spinner">
|
||||
<img src="/static/img/ring.svg">
|
||||
</div>
|
||||
<chromedash-banner
|
||||
message=""
|
||||
timestamp="None">
|
||||
</chromedash-banner>
|
||||
<div id="rollout">
|
||||
<a href="/newfeatures">Try out our new features page</a>
|
||||
</div>
|
||||
<div id="content-flex-wrapper">
|
||||
<div id="content-component-wrapper">
|
||||
|
||||
<div id="subheader">
|
||||
<div>
|
||||
<h2>Feature owners</h2>
|
||||
</div>
|
||||
<a href="/admin/blink">Edit component owners →</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="channels" class="layout horizontal center">
|
||||
<div>
|
||||
<h3 class="channels-title" title="Select a version to only show features for that milestone">Show features in:</h3>
|
||||
<!-- <a href="?showFeatures">show all</a> | <a href="/admin/subscribers">hide all</a> -->
|
||||
</div>
|
||||
|
||||
<div class="chrome_version layout horizontal center chrome_version--stable " title="Show only features for the current stable">
|
||||
<a href="?showFeatures&milestone=1" class="layout horizontal center">
|
||||
<div class="chrome-logo"></div>
|
||||
<!-- <span>stable</span> -->
|
||||
<span class="milestone_number">1</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="chrome_version layout horizontal center chrome_version--beta " title="Show only features for the current beta">
|
||||
<a href="?showFeatures&milestone=2" class="layout horizontal center">
|
||||
<div class="chrome-logo"></div>
|
||||
<!-- <span>beta</span> -->
|
||||
<span class="milestone_number">2</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="chrome_version layout horizontal center chrome_version--dev " title="Show only features for the current dev">
|
||||
<a href="?showFeatures&milestone=3" class="layout horizontal center">
|
||||
<div class="chrome-logo"></div>
|
||||
<!-- <span>dev</span> -->
|
||||
<span class="milestone_number">3</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="chrome_version layout horizontal center chrome_version--canary " title="Show only features for the current canary">
|
||||
<a href="?showFeatures&milestone=4" class="layout horizontal center">
|
||||
<div class="chrome-logo"></div>
|
||||
<!-- <span>canary</span> -->
|
||||
<span class="milestone_number">4</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
|
||||
<ul id="users_list">
|
||||
|
||||
<li>
|
||||
<div>
|
||||
<h3 class="user_name">owner_1</h3>
|
||||
<h4><a href="mailto:owner_1@example.com">owner_1@example.com</a></h4>
|
||||
</div>
|
||||
<div class="component_list">
|
||||
|
||||
<div class="component">
|
||||
<h4><a href="/admin/blink#Blink" class="component_name">Blink</a></h4>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="component">
|
||||
<h4><a href="/admin/blink#Blink>Accessibility" class="component_name">Blink>Accessibility</a></h4>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div>
|
||||
<h3 class="user_name">watcher_1</h3>
|
||||
<h4><a href="mailto:watcher_1@example.com">watcher_1@example.com</a></h4>
|
||||
</div>
|
||||
<div class="component_list">
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<chromedash-footer></chromedash-footer>
|
||||
</div>
|
||||
|
||||
<chromedash-toast msg="Welcome to chromestatus.com!"></chromedash-toast>
|
||||
|
||||
|
||||
|
||||
|
||||
<script nonce="fake nonce">
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.body.classList.remove('loading');
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
<script src="https://www.googletagmanager.com/gtag/js?id=UA-179341418-1"
|
||||
async nonce="fake nonce"></script>
|
||||
|
||||
|
||||
<script type="module" nonce="fake nonce" src="/static/js/shared.min.js?v=Undeployed"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,82 +0,0 @@
|
|||
{% extends "_base.html" %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/static/css/subscribers.css?v={{app_version}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block drawer %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block subheader %}
|
||||
<div id="subheader">
|
||||
<div>
|
||||
<h2>Feature owners</h2>
|
||||
</div>
|
||||
<a href="/admin/blink">Edit component owners →</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="channels" class="layout horizontal center">
|
||||
<div>
|
||||
<h3 class="channels-title" title="Select a version to only show features for that milestone">Show features in:</h3>
|
||||
<!-- <a href="?showFeatures">show all</a> | <a href="/admin/subscribers">hide all</a> -->
|
||||
</div>
|
||||
{% for key,channel in channels.items() %}
|
||||
<div class="chrome_version layout horizontal center chrome_version--{{key}} {% if selected_milestone == channel.mstone %}highlight{% endif %}" title="Show only features for the current {{key}}">
|
||||
<a href="?showFeatures&milestone={{channel.mstone}}" class="layout horizontal center">
|
||||
<div class="chrome-logo"></div>
|
||||
<!-- <span>{{key}}</span> -->
|
||||
<span class="milestone_number">{{channel.mstone}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if selected_milestone %}
|
||||
<div class="filtering">Showing features in M{{selected_milestone}}. <a href="?showFeatures"><b>CLEAR MILESTONE ✘</b></a></div>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
|
||||
<ul id="users_list">
|
||||
{% for s in subscribers %}
|
||||
<li>
|
||||
<div>
|
||||
<h3 class="user_name">{{s.name}}</h3>
|
||||
<h4><a href="mailto:{{s.email}}">{{s.email}}</a></h4>
|
||||
</div>
|
||||
<div class="component_list">
|
||||
{% for component in s.owned_components %}
|
||||
<div class="component">
|
||||
<h4><a href="/admin/blink#{{component.name}}" class="component_name">{{component.name}}</a></h4>
|
||||
{% if component.features|length %}
|
||||
<ol class="component_features">
|
||||
{% for feature in component.features %}
|
||||
<li class="feature_name"><a href="/feature/{{feature.id}}">{{feature.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{nonce}}">
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.body.classList.remove('loading');
|
||||
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
Загрузка…
Ссылка в новой задаче