text: """Copyright 2014 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.
  
  GAE Model for the datastore."""
  
  __author__ = 'joemu@google.com (Joe Allan Muharsky)'
  
  import datetime
  import json
  import logging
  
  from google.appengine.api import users
  from google.appengine.ext import ndb
  
  from perfkit.explorer.util import explorer_config_util
  from perfkit.explorer.util import user_validator
  import dashboard_fields as fields
  
  
  DEFAULT_DASHBOARD_TITLE = 'Untitled Dashboard'
  DEFAULT_DOMAIN = 'google.com'
  
  
  class Error(Exception):
    pass
  
  
  class InitializeError(Error):
    pass
  
  
  class SecurityError(Error):
    pass
  
  
  class Dashboard(ndb.Model):
    """Models a Dashboard definition (as JSON).
  
    Tracks the contents of the dashboard (data, stored as JSON string), plus the
    user who created and last modified the dashboard.
    """
  
    created_by = ndb.UserProperty()
    created_date = ndb.DateTimeProperty()
    modified_by = ndb.UserProperty()
    modified_date = ndb.DateTimeProperty()
    writers = ndb.StringProperty(repeated=True)
    title = ndb.StringProperty(default='')
    data = ndb.TextProperty(default='')
    public = ndb.BooleanProperty(default=False)
  
    @staticmethod
    def GetDashboard(dashboard_id, required=True):
      """Returns a Dashboard model for the provided dashboard_id.
  
      Args:
        dashboard_id: An integer key for a dashboard.
        required: Determines the behavior when the id is not found.  If false,
            None will be returned.  If true, an InitializeError will be raised.
  
      Returns:
        A Dashboard model instance.
  
      Raises:
        InitializeError: If the ID is not found, and required is True.
      """
      dashboard_row = Dashboard.get_by_id(dashboard_id)
  
      if not dashboard_row and required:
        message = 'No dashboard with ID {id} was found.'.format(id=dashboard_id)
        raise InitializeError(message)
  
      return dashboard_row
  
    @staticmethod
    def CopyDashboard(dashboard_id, title=None):
      """Creates a copy of a dashboard, and optionally renames it.
  
      Args:
        dashboard_id: An integer key for a dashboard.
        title: If provided, will be used as the title for the new dashboard.
  
      Returns:
        The id of the newly-created dashboard.
  
      Raises:
        InitializeError: If the ID is not found.
      """
      dashboard_row = Dashboard.get_by_id(dashboard_id)
  
      if not dashboard_row:
        message = 'No dashboard with ID {id} was found.'.format(id=dashboard_id)
        raise InitializeError(message)
  
      data = dashboard_row.GetDashboardData()
  
      new_dashboard = Dashboard()
      new_dashboard.created_by = users.get_current_user()
  
      if title:
        new_dashboard.title = title
        data[fields.TITLE] = title
      else:
        new_dashboard.title = dashboard_row.title
  
      data[fields.WRITERS] = []
  
      new_dashboard.data = json.dumps(data)
      return new_dashboard.put().integer_id()
  
    @staticmethod
    def RenameDashboard(dashboard_id, new_name):
      """Renames a dashboard.
  
      Args:
        dashboard_id: An integer key for a dashboard.
        new_name: The new title to use for the dashboard.
  
      Raises:
        InitializeError: If the ID is not found.
      """
      dashboard_row = Dashboard.get_by_id(dashboard_id)
  
      if not dashboard_row:
        message = 'No dashboard with ID {id} was found.'.format(id=dashboard_id)
        raise InitializeError(message)
  
      data = dashboard_row.GetDashboardData()
  
      data[fields.TITLE] = new_name
      dashboard_row.title = new_name
  
      dashboard_row.data = json.dumps(data)
      dashboard_row.put()
  
    @staticmethod
    def EditDashboardOwner(dashboard_id, owner_email):
      """Renames a dashboard.
  
      Args:
        dashboard_id: An integer key for a dashboard.
        owner_email: The email address of the new owner.  DEFAULT_DOMAIN will be
            applied if no domain (@) is present.
  
      Raises:
        InitializeError: If the ID is not found.
      """
      dashboard_row = Dashboard.get_by_id(dashboard_id)
  
      if not dashboard_row:
        message = 'No dashboard with ID {id} was found.'.format(id=dashboard_id)
        raise InitializeError(message)
  
      owner_email = Dashboard.GetCanonicalEmail(owner_email)
      new_owner = user_validator.UserValidator.GetUserFromEmail(owner_email)
  
      if not new_owner:
        message = 'No owner with email ' + owner_email + ' was found.'.format(
            email=owner_email)
        raise InitializeError(message)
  
      data = dashboard_row.GetDashboardData()
      data['owner'] = new_owner.email()
  
      dashboard_row.created_by = new_owner
      dashboard_row.data = json.dumps(data)
  
      dashboard_row.put()
  
    def writersChanged(self, new_writers):
      """Returns True if the owner or writers have changed, False if not.
  
      Args:
        new_contributors: A list of objects that contain email addresses.
      """
      old_emails = [user for user in self.writers]
      new_emails = [user.get('email') for user in new_writers]
  
      return cmp(old_emails, new_emails) != 0
  
  
    def isOwner(self):
      """Returns True if the current user is an admin, or the owner.
  
      Args:
        user: A GAE user object.
  
      Returns:
        True if the provided user is an owner or admin for the current dashboard.
        Otherwise, false.
      """
      return (
          users.is_current_user_admin() or
          users.get_current_user() == self.created_by)
  
    def isContributor(self):
      """Returns True if any of the data.contributors email addresses is the
      current user.
  
      Args:
        user: A GAE user object.
  
      Returns:
        True if the provided email address exists in data.contributors.
        Otherwise, false.
      """
      email = users.get_current_user().email().lower()
  
      for user_email in self.writers:
        if user_email.lower() == email:
          return True
  
      return False
  
    def canEdit(self):
      """Returns True if the current user is an admin, the owner or a
      contributor.
  
      Args:
        user: A GAE user object.
  
      Returns:
        True if the provided user is an owner, admin or contributor for the
        current dashboard.  Otherwise, false.
      """
      return (self.isOwner() or self.isContributor())
  
    def GetDashboardData(self):
      """Returns a JSON representation of the 'data' field.
  
      Returns:
        A JSON object representation of the dashboard's data.
  
      Raises:
        InitializeError: If the data field contains invalid JSON or doesn't
        exist.
      """
      str_value = self.data
  
      if str_value:
        try:
          return json.loads(str_value)
        except ValueError:
          message = ('The "data" field in dashboard row {id} must be valid JSON.'
                     '  Found:\n{value}').format(id=self.key.integer_id(),
                                                 value=str_value)
          raise InitializeError(message)
      else:
        message = 'The "data" field in dashboard row {id} is required.'.format(
            id=self.key.integer_id())
        raise InitializeError(message)
  
    @staticmethod
    def GetDashboardTitle(dashboard_data):
      """Returns the Dashboard's title, and sets it to a default if not provided.
  
      Args:
        dashboard_data: A JSON representation of the dashboard.
  
      Returns:
        The title of the dashboard.
      """
      if fields.TITLE in dashboard_data:
        title = dashboard_data[fields.TITLE]
      else:
        dashboard_data[fields.TITLE] = DEFAULT_DASHBOARD_TITLE
        title = DEFAULT_DASHBOARD_TITLE
  
      return title
  
    @staticmethod
    def GetDashboardOwner(owner_string):
      """Returns the Dashboard's owner, and sets it to a default if not provided.
  
      Args:
        owner_string: A string that represents the user.
  
      Returns:
        A GAE User object representing the owner of the dashboard.
      """
      if owner_string:
        owner = user_validator.UserValidator.GetUserFromEmail(owner_string)
        if not owner:
          raise users.UserNotFoundError()
      else:
        return users.get_current_user()
  
      return owner
  
    @staticmethod
    def GetCanonicalEmail(owner):
      """Returns a domain-specified email address based on an owner string.
  
      Args:
        owner: An owner name, which may or may not contain a domain.
  
      Returns:
        An email address.  If owner_name does not contain a domain, then
            DEFAULT_OWNER_DOMAIN is applied.
      """
      # TODO: Remove this or update to use GAE app's default domain before
      #     public release.
      if '@' not in owner:
        owner = '{user}@{domain}'.format(user=owner, domain=DEFAULT_DOMAIN)
  
      return owner
  
    @classmethod
    def IsQueryCustom(cls, query, dashboard_id, widget_id):
      try:
        dashboard_model = Dashboard.GetDashboard(int(dashboard_id))
      except InitializeError:
        logging.error('Dashboard %s not found', dashboard_id)
        return True
  
      widget_model = cls.FindWidget(dashboard_model.GetDashboardData(), widget_id)
      if not widget_model:
        return True
  
      try:
        datasource = widget_model.get('datasource')
  
        if not datasource:
          raise KeyError('Datasource not provided')
  
        saved_query = datasource.get('query_exec') or datasource.get('query')
  
        if saved_query:
          if query.strip() == saved_query.strip():
            return False
      except KeyError:
        return True
  
      return True
  
    @classmethod
    def FindWidget(cls, dashboard, widget_id):
      for container in dashboard['children']:
        for widget in container['container']['children']:
          if str(widget['id']) == str(widget_id):
            return widget
  
      return None
  
    @classmethod
    def _pre_delete_hook(cls, key):
      if not explorer_config_util.ExplorerConfigUtil.CanSave():
        raise SecurityError('The current user is not authorized to delete dashboards')
  
    def _pre_put_hook(self):
      if not explorer_config_util.ExplorerConfigUtil.CanSave():
        raise SecurityError('The current user is not authorized to save dashboards')
  
      self.modified_by = users.get_current_user()
      self.modified_date = datetime.datetime.now()
  
      if not self.created_by:
        self.created_by = users.get_current_user()
  
      if not self.created_date:
        self.created_date = datetime.datetime.now()
  
    @classmethod
    def _pre_get_hook(cls, key):
      if not explorer_config_util.ExplorerConfigUtil.CanView():
        raise SecurityError('The current user is not authorized to view dashboards')
  