Merge pull request #495 from GoogleChrome/subscribers
Subscribers and owners
This commit is contained in:
Коммит
9d5eaf080a
5
app.yaml
5
app.yaml
|
@ -65,6 +65,11 @@ handlers:
|
|||
login: required # non-admin
|
||||
secure: always
|
||||
|
||||
- url: /admin/subscribers.*
|
||||
script: blink_handler.app
|
||||
login: required # non-admin
|
||||
secure: always
|
||||
|
||||
- url: /admin/features/.*
|
||||
script: admin.app
|
||||
secure: always
|
||||
|
|
|
@ -30,9 +30,9 @@ import settings
|
|||
import util
|
||||
|
||||
|
||||
class PopulateOwnersHandler(common.ContentHandler):
|
||||
class PopulateSubscribersHandler(common.ContentHandler):
|
||||
|
||||
def __populate_devrel_owers(self):
|
||||
def __populate_subscribers(self):
|
||||
"""Seeds the database with the team in devrel_team.yaml and adds the team
|
||||
member to the specified blink components in that file. Should only be ran
|
||||
if the FeatureOwner database entries have been cleared"""
|
||||
|
@ -42,13 +42,13 @@ class PopulateOwnersHandler(common.ContentHandler):
|
|||
blink_components = [models.BlinkComponent.get_by_name(name).key() for name in blink_components]
|
||||
blink_components = filter(None, blink_components) # Filter out None values
|
||||
|
||||
owner = models.FeatureOwner(
|
||||
user = models.FeatureOwner(
|
||||
name=unicode(profile['name']),
|
||||
email=unicode(profile['email']),
|
||||
twitter=profile.get('twitter', None),
|
||||
blink_components=blink_components
|
||||
)
|
||||
owner.put()
|
||||
user.put()
|
||||
f.close()
|
||||
|
||||
@common.require_whitelisted_user
|
||||
|
@ -56,24 +56,30 @@ class PopulateOwnersHandler(common.ContentHandler):
|
|||
if settings.PROD:
|
||||
return self.response.out.write('Handler not allowed in production.')
|
||||
models.BlinkComponent.update_db()
|
||||
self.__populate_devrel_owers()
|
||||
self.__populate_subscribers()
|
||||
return self.redirect('/admin/blink')
|
||||
|
||||
|
||||
class BlinkHandler(common.ContentHandler):
|
||||
|
||||
def __update_owners_list(self, add=True, user_id=None, blink_component=None):
|
||||
def __update_subscribers_list(self, add=True, user_id=None, blink_component=None, primary=False):
|
||||
if not user_id or not blink_component:
|
||||
return False
|
||||
|
||||
owner = models.FeatureOwner.get_by_id(long(user_id))
|
||||
if not owner:
|
||||
user = models.FeatureOwner.get_by_id(long(user_id))
|
||||
if not user:
|
||||
return True
|
||||
|
||||
if add:
|
||||
owner.add_as_component_owner(blink_component)
|
||||
if primary:
|
||||
if add:
|
||||
user.add_as_component_owner(blink_component)
|
||||
else:
|
||||
user.remove_as_component_owner(blink_component)
|
||||
else:
|
||||
owner.remove_from_component_owners(blink_component)
|
||||
if add:
|
||||
user.add_to_component_subscribers(blink_component)
|
||||
else:
|
||||
user.remove_from_component_subscribers(blink_component)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -85,41 +91,72 @@ class BlinkHandler(common.ContentHandler):
|
|||
# data = memcache.get(key)
|
||||
# if data is None:
|
||||
components = models.BlinkComponent.all().order('name').fetch(None)
|
||||
owners = models.FeatureOwner.all().order('name').fetch(None)
|
||||
subscribers = models.FeatureOwner.all().order('name').fetch(None)
|
||||
|
||||
# Format for django template
|
||||
owners = [x.format_for_template() for x in owners]
|
||||
subscribers = [x.format_for_template() for x in subscribers]
|
||||
|
||||
for c in components:
|
||||
c.primaries = [o.name for o in c.owners]
|
||||
|
||||
# wf_component_content = models.BlinkComponent.fetch_wf_content_for_components()
|
||||
# for c in components:
|
||||
# c.wf_urls = wf_component_content.get(c.name) or []
|
||||
|
||||
data = {
|
||||
'owners': owners,
|
||||
'subscribers': subscribers,
|
||||
'components': components[1:] # ditch generic "Blink" component
|
||||
}
|
||||
# memcache.set(key, data)
|
||||
|
||||
self.render(data, template_path=os.path.join('admin/blink.html'))
|
||||
|
||||
# Remove user from component subscribers.
|
||||
def put(self, path):
|
||||
params = json.loads(self.request.body)
|
||||
self.__update_owners_list(False, user_id=params.get('userId'),
|
||||
blink_component=params.get('componentName'))
|
||||
self.response.set_status(200, message='User removed from owners')
|
||||
self.__update_subscribers_list(False, user_id=params.get('userId'),
|
||||
blink_component=params.get('componentName'),
|
||||
primary=params.get('primary'))
|
||||
self.response.set_status(200, message='User removed from subscribers')
|
||||
return self.response.write(json.dumps({'done': True}))
|
||||
|
||||
# Add user to component subscribers.
|
||||
def post(self, path):
|
||||
params = json.loads(self.request.body)
|
||||
self.__update_owners_list(True, user_id=params.get('userId'),
|
||||
blink_component=params.get('componentName'))
|
||||
|
||||
self.__update_subscribers_list(True, user_id=params.get('userId'),
|
||||
blink_component=params.get('componentName'),
|
||||
primary=params.get('primary'))
|
||||
# memcache.flush_all()
|
||||
# memcache.delete('%s|blinkcomponentowners' % (settings.MEMCACHE_KEY_PREFIX))
|
||||
self.response.set_status(200, message='User added to owners')
|
||||
self.response.set_status(200, message='User added to subscribers')
|
||||
return self.response.write(json.dumps(params))
|
||||
|
||||
|
||||
class SubscribersHandler(common.ContentHandler):
|
||||
|
||||
@common.require_whitelisted_user
|
||||
# @common.strip_trailing_slash
|
||||
def get(self, path):
|
||||
subscribers = models.FeatureOwner.all().order('name').fetch(None)
|
||||
|
||||
# Format for django template
|
||||
# subscribers = [x.format_for_template() for x in subscribers]
|
||||
|
||||
for s in subscribers:
|
||||
s.subscribed_components = [models.BlinkComponent.get(key) for key in s.blink_components]
|
||||
s.owned_components = [models.BlinkComponent.get(key) for key in s.primary_blink_components]
|
||||
|
||||
data = {
|
||||
'subscribers': subscribers,
|
||||
}
|
||||
|
||||
self.render(data, template_path=os.path.join('admin/subscribers.html'))
|
||||
|
||||
|
||||
app = webapp2.WSGIApplication([
|
||||
('/admin/blink/populate_owners', PopulateOwnersHandler),
|
||||
('/admin/blink/populate_subscribers', PopulateSubscribersHandler),
|
||||
('/admin/subscribers(.*)', SubscribersHandler),
|
||||
('(.*)', BlinkHandler),
|
||||
], debug=settings.DEBUG)
|
||||
|
||||
|
|
|
@ -158,6 +158,11 @@ indexes:
|
|||
- name: blink_components
|
||||
- name: name
|
||||
|
||||
- kind: FeatureOwner
|
||||
properties:
|
||||
- name: primary_blink_components
|
||||
- name: name
|
||||
|
||||
- kind: StableInstance
|
||||
properties:
|
||||
- name: bucket_id
|
||||
|
|
63
models.py
63
models.py
|
@ -229,10 +229,13 @@ class BlinkComponent(DictModel):
|
|||
created = db.DateTimeProperty(auto_now_add=True)
|
||||
updated = db.DateTimeProperty(auto_now=True)
|
||||
|
||||
@property
|
||||
def subscribers(self):
|
||||
return FeatureOwner.all().filter('blink_components = ', self.key()).order('name').fetch(None)
|
||||
|
||||
@property
|
||||
def owners(self):
|
||||
q = FeatureOwner.all().filter('blink_components = ', self.key()).order('name')
|
||||
return q.fetch(None)
|
||||
return FeatureOwner.all().filter('primary_blink_components = ', self.key()).order('name').fetch(None)
|
||||
|
||||
@classmethod
|
||||
def fetch_all_components(self, update_cache=False):
|
||||
|
@ -289,6 +292,7 @@ class BlinkComponent(DictModel):
|
|||
return None
|
||||
return component[0]
|
||||
|
||||
|
||||
# UMA metrics.
|
||||
class StableInstance(DictModel):
|
||||
created = db.DateTimeProperty(auto_now_add=True)
|
||||
|
@ -743,8 +747,8 @@ class Feature(DictModel):
|
|||
old_val = getattr(self, prop_name, None)
|
||||
setattr(self, '_old_' + prop_name, old_val)
|
||||
|
||||
def __notify_feature_owners_of_changes(self, is_update):
|
||||
"""Async notifies owners of new features and property changes to features by
|
||||
def __notify_feature_subscribers_of_changes(self, is_update):
|
||||
"""Async notifies subscribers of new features and property changes to features by
|
||||
posting to a task queue."""
|
||||
# Diff values to see what properties have changed.
|
||||
changed_props = []
|
||||
|
@ -761,9 +765,9 @@ class Feature(DictModel):
|
|||
'feature': self.format_for_template(version=2)
|
||||
})
|
||||
|
||||
# Create task to email owners.
|
||||
# Create task to email subscribers.
|
||||
queue = taskqueue.Queue()#name='emailer')
|
||||
task = taskqueue.Task(method="POST", url='/tasks/email-owners',
|
||||
task = taskqueue.Task(method="POST", url='/tasks/email-subscribers',
|
||||
target='notifier', payload=payload)
|
||||
queue.add(task)
|
||||
|
||||
|
@ -777,7 +781,7 @@ class Feature(DictModel):
|
|||
def put(self, **kwargs):
|
||||
is_update = self.is_saved()
|
||||
key = super(Feature, self).put(**kwargs)
|
||||
self.__notify_feature_owners_of_changes(is_update)
|
||||
self.__notify_feature_subscribers_of_changes(is_update)
|
||||
return key
|
||||
|
||||
# Metadata.
|
||||
|
@ -1009,33 +1013,62 @@ class AppUser(DictModel):
|
|||
updated = db.DateTimeProperty(auto_now=True)
|
||||
|
||||
|
||||
def list_with_component(l, component):
|
||||
return [x for x in l if x.id() == component.key().id()]
|
||||
|
||||
def list_without_component(l, component):
|
||||
return [x for x in l if x.id() != component.key().id()]
|
||||
|
||||
|
||||
class FeatureOwner(DictModel):
|
||||
"""Describes owner of a web platform feature."""
|
||||
"""Describes subscribers of a web platform feature."""
|
||||
created = db.DateTimeProperty(auto_now_add=True)
|
||||
updated = db.DateTimeProperty(auto_now=True)
|
||||
name = db.StringProperty(required=True)
|
||||
email = db.EmailProperty(required=True)
|
||||
twitter = db.StringProperty()
|
||||
blink_components = db.ListProperty(db.Key)
|
||||
primary_blink_components = db.ListProperty(db.Key)
|
||||
watching_all_features = db.BooleanProperty(default=False)
|
||||
|
||||
def add_as_component_owner(self, component_name):
|
||||
"""Adds the user to the list of Blink component owners."""
|
||||
def add_to_component_subscribers(self, component_name):
|
||||
"""Adds the user to the list of Blink component subscribers."""
|
||||
c = BlinkComponent.get_by_name(component_name)
|
||||
if c:
|
||||
already_added = len([x for x in self.blink_components if x.id() == c.key().id()])
|
||||
if not already_added:
|
||||
# Add the user if they're not already in the list.
|
||||
if not len(list_with_component(self.blink_components, c)):
|
||||
self.blink_components.append(c.key())
|
||||
return self.put()
|
||||
return None
|
||||
|
||||
def remove_from_component_owners(self, component_name):
|
||||
"""Removes the user from the list of Blink component owners."""
|
||||
def remove_from_component_subscribers(self, component_name, remove_as_owner=False):
|
||||
"""Removes the user from the list of Blink component subscribers or as the owner
|
||||
of the component."""
|
||||
c = BlinkComponent.get_by_name(component_name)
|
||||
if c:
|
||||
self.blink_components = [x for x in self.blink_components if x.id() != c.key().id()]
|
||||
if remove_as_owner:
|
||||
self.primary_blink_components = list_without_component(self.primary_blink_components, c)
|
||||
else:
|
||||
self.blink_components = list_without_component(self.blink_components, c)
|
||||
self.primary_blink_components = list_without_component(self.primary_blink_components, c)
|
||||
return self.put()
|
||||
return None
|
||||
|
||||
def add_as_component_owner(self, component_name):
|
||||
"""Adds the user as the Blink component owner."""
|
||||
c = BlinkComponent.get_by_name(component_name)
|
||||
if c:
|
||||
# Update both the primary list and blink components subscribers if the
|
||||
# user is not already in them.
|
||||
self.add_to_component_subscribers(component_name)
|
||||
if not len(list_with_component(self.primary_blink_components, c)):
|
||||
self.primary_blink_components.append(c.key())
|
||||
return self.put()
|
||||
return None
|
||||
|
||||
def remove_as_component_owner(self, component_name):
|
||||
return self.remove_from_component_subscribers(component_name, remove_as_owner=True)
|
||||
|
||||
|
||||
class HistogramModel(db.Model):
|
||||
"""Container for a histogram."""
|
||||
|
|
61
notifier.py
61
notifier.py
|
@ -56,19 +56,27 @@ def create_wf_content_list(component):
|
|||
list = '<li>None</li>'
|
||||
return list
|
||||
|
||||
def email_feature_owners(feature, is_update=False, changes=[]):
|
||||
def email_feature_subscribers(feature, is_update=False, changes=[]):
|
||||
feature_watchers = models.FeatureOwner.all().filter('watching_all_features = ', True).fetch(None)
|
||||
|
||||
for component_name in feature.blink_components:
|
||||
component = models.BlinkComponent.get_by_name(component_name)
|
||||
if not component:
|
||||
logging.warn('Blink component "%s" not found. Not sending email to owners' % component_name)
|
||||
logging.warn('Blink component "%s" not found. Not sending email to subscribers' % component_name)
|
||||
return
|
||||
|
||||
# owners = component.owners
|
||||
# TODO: restrict emails to me for now to see if they're not too noisy.
|
||||
owners = models.FeatureOwner.all().filter('email = ', 'e.bidelman@google.com').fetch(1)
|
||||
def list_diff(subscribers, owners):
|
||||
"""Returns list B - A."""
|
||||
owner_ids = [x.key().id() for x in owners]
|
||||
return [x for x in subscribers if not x.key().id() in owner_ids]
|
||||
|
||||
if not owners:
|
||||
logging.info('Blink component "%s" has no owners. Skipping email.' % component_name)
|
||||
# TODO: switch back
|
||||
owners = [] #component.owners
|
||||
# subscribers = list_diff(component.subscribers, owners) + feature_watchers
|
||||
subscribers = models.FeatureOwner.all().filter('email = ', 'e.bidelman@google.com').fetch(1)
|
||||
|
||||
if not subscribers and not owners:
|
||||
logging.info('Blink component "%s" has no subscribers or owners. Skipping email.' % component_name)
|
||||
return
|
||||
|
||||
if feature.shipped_milestone:
|
||||
|
@ -78,12 +86,16 @@ def email_feature_owners(feature, is_update=False, changes=[]):
|
|||
else:
|
||||
milestone_str = 'not yet assigned'
|
||||
|
||||
intro = 'You are listed as an owner for web platform features under "{component_name}"'.format(component_name=component_name)
|
||||
if not owners:
|
||||
intro = 'Just letting you know that there\'s a new feature under "{component_name}".'.format(component_name=component_name)
|
||||
|
||||
created_on = datetime.datetime.strptime(str(feature.created), "%Y-%m-%d %H:%M:%S.%f").date()
|
||||
new_msg = """
|
||||
<html><body>
|
||||
<p>Hi {owners},</p>
|
||||
|
||||
<p>You are listed as a web platform owner for "{component_name}". {created_by} added a new feature to this component:</p>
|
||||
<p>{intro}. {created_by} added a new feature to this component:</p>
|
||||
<hr>
|
||||
|
||||
<p><b><a href="https://www.chromestatus.com/feature/{id}">{name}</a></b> (added {created})</p>
|
||||
|
@ -99,10 +111,11 @@ def email_feature_owners(feature, is_update=False, changes=[]):
|
|||
<li>Add a sample to https://github.com/GoogleChrome/samples (see <a href="https://github.com/GoogleChrome/samples#contributing-samples">contributing</a>).</li>
|
||||
<li>Don't forget add your demo link to the <a href="https://www.chromestatus.com/admin/features/edit/{id}">chromestatus feature entry</a>.</li>
|
||||
</ul>
|
||||
<p>CC'd on this email? Feel free to reply-all if you can help with these tasks.</p>
|
||||
</body></html>
|
||||
""".format(name=feature.name, id=feature.key().id(), created=created_on,
|
||||
created_by=feature.created_by, component_name=component_name,
|
||||
owners=', '.join([owner.name for owner in owners]), milestone=milestone_str,
|
||||
created_by=feature.created_by, intro=intro,
|
||||
owners=', '.join([o.name for o in owners]), milestone=milestone_str,
|
||||
status=models.IMPLEMENTATION_STATUS[feature.impl_status_chrome])
|
||||
|
||||
updated_on = datetime.datetime.strptime(str(feature.updated), "%Y-%m-%d %H:%M:%S.%f").date()
|
||||
|
@ -112,10 +125,14 @@ def email_feature_owners(feature, is_update=False, changes=[]):
|
|||
if not formatted_changes:
|
||||
formatted_changes = '<li>None</li>'
|
||||
|
||||
intro = 'You are listed as an owner for web platform features under "{component_name}"'.format(component_name=component_name)
|
||||
if not owners:
|
||||
intro = 'Just letting you know that a feature under "{component_name}" has changed.'.format(component_name=component_name)
|
||||
|
||||
update_msg = """<html><body>
|
||||
<p>Hi {owners},</p>
|
||||
|
||||
<p>You are listed as a web platform owner for "{component_name}". {updated_by} updated this feature:</p>
|
||||
<p>{intro}. {updated_by} updated this feature:</p>
|
||||
<hr>
|
||||
|
||||
<p><b><a href="https://www.chromestatus.com/feature/{id}">{name}</a></b> (updated {updated})</p>
|
||||
|
@ -135,17 +152,23 @@ def email_feature_owners(feature, is_update=False, changes=[]):
|
|||
<ul>{wf_content}</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>CC'd on this email? Feel free to reply-all if can help.</p>
|
||||
</body></html>
|
||||
""".format(name=feature.name, id=feature.key().id(), updated=updated_on,
|
||||
updated_by=feature.updated_by, component_name=component_name,
|
||||
owners=', '.join([owner.name for owner in owners]), milestone=milestone_str,
|
||||
updated_by=feature.updated_by, intro=intro,
|
||||
owners=', '.join([o.name for o in owners]), milestone=milestone_str,
|
||||
status=models.IMPLEMENTATION_STATUS[feature.impl_status_chrome],
|
||||
formatted_changes=formatted_changes,
|
||||
wf_content=create_wf_content_list(component_name))
|
||||
|
||||
message = mail.EmailMessage(sender='Chromestatus <admin@cr-status.appspotmail.com>',
|
||||
subject='update',
|
||||
to=[owner.email for owner in owners])
|
||||
cc=[s.email for s in subscribers])
|
||||
|
||||
# Only include to: line if there are feature owners. Otherwise, we'll just use cc.
|
||||
if owners:
|
||||
message.to = [s.email for s in owners]
|
||||
|
||||
if is_update:
|
||||
message.html = update_msg
|
||||
|
@ -160,7 +183,7 @@ def email_feature_owners(feature, is_update=False, changes=[]):
|
|||
message.send()
|
||||
|
||||
|
||||
class EmailOwnersHandler(webapp2.RequestHandler):
|
||||
class EmailHandler(webapp2.RequestHandler):
|
||||
|
||||
def post(self):
|
||||
json_body = json.loads(self.request.body)
|
||||
|
@ -168,10 +191,10 @@ class EmailOwnersHandler(webapp2.RequestHandler):
|
|||
is_update = json_body.get('is_update') or False
|
||||
changes = json_body.get('changes') or []
|
||||
|
||||
# Email feature owners if the feature exists and there were actually changes to it.
|
||||
# Email feature subscribers if the feature exists and there were actually changes to it.
|
||||
feature = models.Feature.get_by_id(feature['id'])
|
||||
if feature and (is_update and len(changes) or not is_update):
|
||||
email_feature_owners(feature, is_update=is_update, changes=changes)
|
||||
email_feature_subscribers(feature, is_update=is_update, changes=changes)
|
||||
|
||||
|
||||
class NotificationNewSubscriptionHandler(webapp2.RequestHandler):
|
||||
|
@ -260,7 +283,7 @@ class NotificationSendHandler(webapp2.RequestHandler):
|
|||
is_update = json_body.get('is_update') or False
|
||||
changes = json_body.get('changes') or []
|
||||
|
||||
# Email feature owners if the feature exists and there were changes to it.
|
||||
# Email feature subscribers if the feature exists and there were changes to it.
|
||||
feature = models.Feature.get_by_id(feature['id'])
|
||||
if feature and (is_update and len(changes) or not is_update):
|
||||
self._send_notification_to_feature_subscribers(feature=feature, is_update=is_update)
|
||||
|
@ -299,7 +322,7 @@ class NotificationsListHandler(common.ContentHandler):
|
|||
|
||||
app = webapp2.WSGIApplication([
|
||||
('/admin/notifications/list', NotificationsListHandler),
|
||||
('/tasks/email-owners', EmailOwnersHandler),
|
||||
('/tasks/email-subscribers', EmailHandler),
|
||||
('/tasks/send_notifications', NotificationSendHandler),
|
||||
('/features/push/new', NotificationNewSubscriptionHandler),
|
||||
('/features/push/info', NotificationSubscriptionInfoHandler),
|
||||
|
|
|
@ -39,6 +39,9 @@ body {
|
|||
}
|
||||
}
|
||||
}
|
||||
.view_owners_linke {
|
||||
margin-left: $content-padding;
|
||||
}
|
||||
}
|
||||
#components_list {
|
||||
list-style: none;
|
||||
|
@ -79,6 +82,9 @@ body {
|
|||
.owners_list {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.component_owner {
|
||||
font-weight: 600;
|
||||
}
|
||||
.owners_list_add_remove {
|
||||
margin-left: $content-padding / 2;
|
||||
opacity: 0;
|
||||
|
@ -95,7 +101,7 @@ body {
|
|||
}
|
||||
|
||||
select[multiple] {
|
||||
min-width: 125px;
|
||||
min-width: 275px;
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
transition: 200ms background-color cubic-bezier(0,0,0.2,1);
|
||||
|
|
|
@ -16,12 +16,15 @@
|
|||
|
||||
{% block subheader %}
|
||||
<div id="subheader">
|
||||
<div>
|
||||
<h2>blink component owners</h2>
|
||||
<div>listing {{components|length}} components</div>
|
||||
<div class="layout horizontal center">
|
||||
<div>
|
||||
<h2>Blink components</h2>
|
||||
<div>listing {{components|length}} components</div>
|
||||
</div>
|
||||
<a href="/admin/subscribers" class="view_owners_linke">View list by owner →</a>
|
||||
</div>
|
||||
<div class="layout horizontal subheader_toggles">
|
||||
<paper-toggle-button noink>Edit owners</paper-toggle-button>
|
||||
<paper-toggle-button noink>Edit mode</paper-toggle-button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -60,20 +63,24 @@
|
|||
</div>
|
||||
<div class="owners_list layout horizontal center">
|
||||
<div>
|
||||
<div class="column_header">Owners</div>
|
||||
<select multiple disabled id="owner_list_{{forloop.counter}}" size="{{c.owners|length}}">
|
||||
{% for owner in c.owners %}
|
||||
<option class="owner_name" value="{{owner.id}}">{{owner.name}}</option>
|
||||
<div class="column_header">Receives email updates:</div>
|
||||
<select multiple disabled id="owner_list_{{forloop.counter}}" size="{{c.subscribers|length}}">
|
||||
{% for s in c.subscribers %}
|
||||
<option class="owner_name {% if s.name in c.primaries %}component_owner{% endif %}"
|
||||
value="{{s.id}}">{{s.name}}: {{s.email}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="owners_list_add_remove">
|
||||
<select class="owner_candidates">
|
||||
<option selected disabled>Select owner to add/remove</option>
|
||||
{% for owner in owners %}
|
||||
<option class="owner_name" value="{{owner.id}}">{{owner.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div>
|
||||
<select class="owner_candidates">
|
||||
<option selected disabled>Select owner to add/remove</option>
|
||||
{% for s in subscribers %}
|
||||
<option class="owner_name" value="{{s.id}}" data-email="{{s.email}}">{{s.name}}</option>
|
||||
{% endfor %}
|
||||
</select><br>
|
||||
<label title="Toggles the user as an owner. If you click 'Remove' ans this is not checked, the user is removed from the component.">Owner? <input type="checkbox" class="is_primary_checkbox"></label>
|
||||
</div>
|
||||
<button class="add_owner_button" data-index="{{forloop.counter}}"
|
||||
data-component-name="{{c.name}}">Add</button>
|
||||
<button class="remove_owner_button" data-index="{{forloop.counter}}"
|
||||
|
@ -107,11 +114,14 @@ $('#components_list').addEventListener('click', function(e) {
|
|||
}
|
||||
|
||||
const candidates = e.target.parentElement.querySelector('.owner_candidates');
|
||||
const primaryCheckbox = e.target.parentElement.querySelector('.is_primary_checkbox');
|
||||
const idx = e.target.dataset.index;
|
||||
const componentName = e.target.dataset.componentName;
|
||||
const userId = candidates.value;
|
||||
const selectedCandidate = candidates.selectedOptions[0];
|
||||
const username = selectedCandidate.textContent;
|
||||
const email = selectedCandidate.dataset.email;
|
||||
const toggleAsOwner = primaryCheckbox.checked;
|
||||
|
||||
if (selectedCandidate.disabled) {
|
||||
alert('Please select a user');
|
||||
|
@ -120,40 +130,47 @@ $('#components_list').addEventListener('click', function(e) {
|
|||
|
||||
// Add new item to owners list.
|
||||
const ownersList = this.querySelector(`#owner_list_${idx}`);
|
||||
const foundName = Array.from(ownersList.options).find(option => option.textContent === username);
|
||||
let doFetch = false;
|
||||
const optionText = `${username}: ${email}`;
|
||||
const foundName = Array.from(ownersList.options).find(option => option.textContent === optionText);
|
||||
|
||||
if (addUser) {
|
||||
// Don't try to add user if they're already in the list, and we're not
|
||||
// modifying their owner state.
|
||||
if (foundName && !toggleAsOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = userId;
|
||||
option.textContent = optionText;
|
||||
|
||||
if (!foundName) {
|
||||
const option = document.createElement('option');
|
||||
option.value = userId;
|
||||
option.textContent = candidates.selectedOptions[0].textContent;
|
||||
ownersList.appendChild(option);
|
||||
doFetch = true;
|
||||
}
|
||||
|
||||
if (toggleAsOwner) {
|
||||
const el = foundName || option;
|
||||
el.classList.add('component_owner');
|
||||
}
|
||||
} else if (removeUser && foundName) {
|
||||
foundName.remove(); // remove existing name.
|
||||
doFetch = true;
|
||||
}
|
||||
|
||||
if (!doFetch) {
|
||||
return;
|
||||
if (toggleAsOwner) {
|
||||
foundName.classList.remove('component_owner');
|
||||
} else {
|
||||
foundName.remove(); // remove existing name.
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/admin/blink', {
|
||||
method: removeUser ? 'PUT' : 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({userId, componentName})
|
||||
body: JSON.stringify({userId, componentName, primary: toggleAsOwner})
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.then(json => {
|
||||
const Toast = document.querySelector('chromedash-toast');
|
||||
if (addUser) {
|
||||
Toast.showMessage(`User added to "${componentName}" owners.`);
|
||||
} else if (removeUser) {
|
||||
Toast.showMessage(`User removed from "${componentName}" owners.`);
|
||||
}
|
||||
Toast.showMessage(`"${componentName}" updated.`);
|
||||
ownersList.size = ownersList.options.length;
|
||||
primaryCheckbox.checked = false;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
{% extends "base.html" %}
|
||||
{% load verbatim %}
|
||||
{% load inline_file %}
|
||||
|
||||
{% block html_imports %}
|
||||
<link rel="import" href="/static/elements/admin-imports{% if VULCANIZE %}.vulcanize{% endif %}.html">
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<!-- <style>{% inline_file "/static/css/blink.css" %}</style> -->
|
||||
<style>
|
||||
:root {
|
||||
--padding: 8px;
|
||||
}
|
||||
#users_list {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
#users_list > li {
|
||||
padding: var(--padding) 0;
|
||||
}
|
||||
.component_list {
|
||||
list-style: none;
|
||||
padding: var(--padding) calc(var(--padding) * 2);
|
||||
}
|
||||
.component_list li {
|
||||
margin-left: calc(var(--padding) * 2);
|
||||
}
|
||||
.layout.horizontal {
|
||||
display: flex;
|
||||
}
|
||||
.layout.horizontal.center {
|
||||
align-items: center;
|
||||
}
|
||||
#subheader a {
|
||||
margin-left: calc(var(--padding) * 3);
|
||||
}
|
||||
.user_name {
|
||||
min-width: 175px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block drawer %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block subheader %}
|
||||
<div id="subheader">
|
||||
<div>
|
||||
<h2>Feature owners</h2>
|
||||
</div>
|
||||
<a href="/admin/blink">View list as components →</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section>
|
||||
|
||||
<ul id="users_list">
|
||||
{% for s in subscribers %}
|
||||
<li class="layout horizontal center">
|
||||
<div>
|
||||
<h3 class="user_name">{{s.name}}</h3>
|
||||
<h4><a href="mailto:{{s.email}}">{{s.email}}</a></h4>
|
||||
</div>
|
||||
<ul class="component_list layout horizontal center">
|
||||
{% for c in s.owned_components %}
|
||||
<li><a href="/admin/blink#{{c.name}}">{{c.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.body.classList.remove('loading');
|
||||
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
Загрузка…
Ссылка в новой задаче