text:
"""GDataService provides CRUD ops. and programmatic login for GData services.

  Error: A base exception class for all exceptions in the gdata_client
         module.

  CaptchaRequired: This exception is thrown when a login attempt results in a
                   captcha challenge from the ClientLogin service. When this
                   exception is thrown, the captcha_token and captcha_url are
                   set to the values provided in the server's response.

  BadAuthentication: Raised when a login attempt is made with an incorrect
                     username or password.

  NotAuthenticated: Raised if an operation requiring authentication is called
                    before a user has authenticated.

  NonAuthSubToken: Raised if a method to modify an AuthSub token is used when
                   the user is either not authenticated or is authenticated
                   through programmatic login.

  RequestError: Raised if a CRUD request returned a non-success code.

  UnexpectedReturnType: Raised if the response from the server was not of the
                        desired type. For example, this would be raised if the
                        server sent a feed when the client requested an entry.

  GDataService: Encapsulates user credentials needed to perform insert, update
                and delete operations with the GData API. An instance can
                perform user authentication, query, insertion, deletion, and
                update.

  Query: Eases query URI creation by allowing URI parameters to be set as
         dictionary attributes. For example a query with a feed of
         '/base/feeds/snippets' and ['bq'] set to 'digital camera' will
         produce '/base/feeds/snippets?bq=digital+camera' when .ToUri() is
         called on it.
"""

__author__ = 'api.jscudder (Jeffrey Scudder)'

import re
import httplib
import urllib
try:
  from xml.etree import cElementTree as ElementTree
except ImportError:
  try:
    import cElementTree as ElementTree
  except ImportError:
    from elementtree import ElementTree
import atom.service
import gdata
import atom

PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth'
AUTHSUB_AUTH_LABEL = 'AuthSub token'
AUTH_SERVER_HOST = 'https://www.google.com/'

class Error(Exception):
  pass

class CaptchaRequired(Error):
  pass

class BadAuthentication(Error):
  pass

class NotAuthenticated(Error):
  pass

class NonAuthSubToken(Error):
  pass

class RequestError(Error):
  pass

class UnexpectedReturnType(Error):
  pass

class GDataService(atom.service.AtomService):
  """Contains elements needed for GData login and CRUD request headers.

  Maintains additional headers (tokens for example) needed for the GData
  services to allow a user to perform inserts, updates, and deletes.
  """

  def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE',
               service=None, source=None, server=None,
               additional_headers=None):
    """Creates an object of type GDataService.

    Args:
      email: string (optional) The user's email address, used for
             authentication.
      password: string (optional) The user's password.
      account_type: string (optional) The type of account to use. Use
              'GOOGLE' for regular Google accounts or 'HOSTED' for Google
              Apps accounts, or 'HOSTED_OR_GOOGLE' to try finding a HOSTED
              account first and, if it doesn't exist, try finding a regular
              GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'.
      service: string (optional) The desired service for which credentials
               will be obtained.
      source: string (optional) The name of the user's application.
      server: string (optional) The name of the server to which a connection
              will be opened. Default value: 'base.google.com'.
      additional_headers: dictionary (optional) Any additional headers which
                          should be included with CRUD operations.
    """

    self.email = email
    self.password = password
    self.account_type = account_type
    self.service = service
    self.server = server
    self.additional_headers = additional_headers or {}
    self.__SetSource(source)
    self.__auth_token = None
    self.__auth_type = None
    self.__captcha_token = None
    self.__captcha_url = None
    self.__gsessionid = None

  # Define properties for GDataService
  def _SetAuthSubToken(self, auth_token):
    """Sets the token sent in requests to an AuthSub token.

    Only use this method if you have received a token from the AuthSub
    service. The auth_token is set automatically when ProgrammaticLogin()
    is used. See documentation for Google AuthSub here:
    http://code.google.com/apis/accounts/AuthForWebApps.html .

    Args:
      auth_token: string The token returned by the AuthSub service.
    """

    self.__auth_token = auth_token
    # The auth token is only set externally when using AuthSub authentication,
    # so set the auth_type to indicate AuthSub.
    self.__auth_type = AUTHSUB_AUTH_LABEL

  def __SetAuthSubToken(self, auth_token):
    self._SetAuthSubToken(auth_token)

  def _GetAuthToken(self):
    """Returns the auth token used for authenticating requests.

    Returns:
      string
    """

    return self.__auth_token

  def __GetAuthToken(self):
    return self._GetAuthToken()

  auth_token = property(__GetAuthToken, __SetAuthSubToken,
      doc="""Get or set the token used for authentication.""")

  def _GetCaptchaToken(self):
    """Returns a captcha token if the most recent login attempt generated one.

    The captcha token is only set if the Programmatic Login attempt failed
    because the Google service issued a captcha challenge.

    Returns:
      string
    """

    return self.__captcha_token

  def __GetCaptchaToken(self):
    return self._GetCaptchaToken()

  captcha_token = property(__GetCaptchaToken,
      doc="""Get the captcha token for a login request.""")

  def _GetCaptchaURL(self):
    """Returns the URL of the captcha image if a login attempt generated one.

    The captcha URL is only set if the Programmatic Login attempt failed
    because the Google service issued a captcha challenge.

    Returns:
      string
    """

    return self.__captcha_url

  def __GetCaptchaURL(self):
    return self._GetCaptchaURL()

  captcha_url = property(__GetCaptchaURL,
      doc="""Get the captcha URL for a login request.""")

  def GetAuthSubToken(self):
    if self.__auth_type == AUTHSUB_AUTH_LABEL:
      return self.__auth_token
    else:
      return None

  def SetAuthSubToken(self, token):
    self.__auth_token = token
    self.__auth_type = AUTHSUB_AUTH_LABEL

  def GetClientLoginToken(self):
    if self.__auth_type == PROGRAMMATIC_AUTH_LABEL:
      return self.__auth_token
    else:
      return None

  def SetClientLoginToken(self, token):
    self.__auth_token = token
    self.__auth_type = PROGRAMMATIC_AUTH_LABEL

  # Private methods to create the source property.
  def __GetSource(self):
    return self.__source

  def __SetSource(self, new_source):
    self.__source = new_source
    # Update the UserAgent header to include the new application name.
    self.additional_headers['User-Agent'] = '%s GData-Python/1.0.9' % self.__source

  source = property(__GetSource, __SetSource,
      doc="""The source is the name of the application making the request.
             It should be in the form company_id-app_name-app_version""")

  # Authentication operations

  def ProgrammaticLogin(self, captcha_token=None, captcha_response=None):
    """Authenticates the user and sets the GData Auth token.

    Login retreives a temporary auth token which must be used with all
    requests to GData services. The auth token is stored in the GData client
    object.

    Login is also used to respond to a captcha challenge. If the user's login
    attempt failed with a CaptchaRequired error, the user can respond by
    calling Login with the captcha token and the answer to the challenge.

    Args:
      captcha_token: string (optional) The identifier for the captcha challenge
                     which was presented to the user.
      captcha_response: string (optional) The user's answer to the captch
                        challenge.

    Raises:
      CaptchaRequired if the login service will require a captcha response
      BadAuthentication if the login service rejected the username or password
      Error if the login service responded with a 403 different from the above
    """

    # Create a POST body containing the user's credentials.
    if captcha_token and captcha_response:
      # Send the captcha token and response as part of the POST body if the
      # user is responding to a captch challenge.
      request_body = urllib.urlencode({'Email': self.email,
                                       'Passwd': self.password,
                                       'accountType': self.account_type,
                                       'service': self.service,
                                       'source': self.source,
                                       'logintoken': captcha_token,
                                       'logincaptcha': captcha_response})
    else:
      request_body = urllib.urlencode({'Email': self.email,
                                       'Passwd': self.password,
                                       'accountType': self.account_type,
                                       'service': self.service,
                                       'source': self.source})

    # Open a connection to the authentication server.
    (auth_connection, uri) = self._PrepareConnection(AUTH_SERVER_HOST)

    # Begin the POST request to the client login service.
    auth_connection.putrequest('POST', '/accounts/ClientLogin')
    # Set the required headers for an Account Authentication request.
    auth_connection.putheader('Content-type',
                              'application/x-www-form-urlencoded')
    auth_connection.putheader('Content-Length',str(len(request_body)))
    auth_connection.endheaders()

    auth_connection.send(request_body)

    # Process the response and throw exceptions if the login request did not
    # succeed.
    auth_response = auth_connection.getresponse()

    if auth_response.status == 200:
      response_body = auth_response.read()
      for response_line in response_body.splitlines():
        if response_line.startswith('Auth='):
          self.__auth_token = response_line.lstrip('Auth=')
          self.__auth_type = PROGRAMMATIC_AUTH_LABEL
          # Get rid of any residual captcha information because the request
          # succeeded.
          self.__captcha_token = None
          self.__captcha_url = None

    elif auth_response.status == 403:
      response_body = auth_response.read()
      # Examine each line to find the error type and the captcha token and
      # captch URL if they are present.
      for response_line in response_body.splitlines():
        if response_line.startswith('Error='):
          error_line = response_line
        elif response_line.startswith('CaptchaToken='):
          self.__captcha_token = response_line.lstrip('CaptchaToken=')
        elif response_line.startswith('CaptchaUrl='):
          self.__captcha_url = '%saccounts/Captcha%s' % (AUTH_SERVER_HOST,
                               response_line.lstrip('CaptchaUrl='))

      # Raise an exception based on the error type in the 403 response.
      # In cases where there was no captcha challenge, remove any previous
      # captcha values.
      if error_line == 'Error=CaptchaRequired':
        raise CaptchaRequired, 'Captcha Required'
      elif error_line == 'Error=BadAuthentication':
        self.__captcha_token = None
        self.__captcha_url = None
        raise BadAuthentication, 'Incorrect username or password'
      else:
        self.__captcha_token = None
        self.__captcha_url = None
        raise Error, 'Server responded with a 403 code'

  def ClientLogin(self, username, password, account_type=None, service=None,
      source=None, captcha_token=None, captcha_response=None):
    """Convenience method for authenticating using ProgrammaticLogin.

    Sets values for email, password, and other optional members.

    Args:
      username:
      password:
      account_type: string (optional)
      service: string (optional)
      captcha_token: string (optional)
      captcha_response: string (optional)
    """
    self.email = username
    self.password = password

    if account_type:
      self.account_type = account_type
    if service:
      self.service = service
    if source:
      self.source = source

    self.ProgrammaticLogin(captcha_token, captcha_response)

  def GenerateAuthSubURL(self, next, scope, secure=False, session=True):
    """Generate a URL at which the user will login and be redirected back.

    Users enter their credentials on a Google login page and a token is sent
    to the URL specified in next. See documentation for AuthSub login at:
    http://code.google.com/apis/accounts/AuthForWebApps.html

    Args:
      next: string The URL user will be sent to after logging in.
      scope: string The URL of the service to be accessed.
      secure: boolean (optional) Determines whether or not the issued token
              is a secure token.
      session: boolean (optional) Determines whether or not the issued token
               can be upgraded to a session token.
    """

    # Translate True/False values for parameters into numeric values acceoted
    # by the AuthSub service.
    if secure:
      secure = 1
    else:
      secure = 0

    if session:
      session = 1
    else:
      session = 0

    request_params = urllib.urlencode({'next': next, 'scope': scope,
                                    'secure': secure, 'session': session})
    return '%saccounts/AuthSubRequest?%s' % (AUTH_SERVER_HOST, request_params)

  def UpgradeToSessionToken(self):
    """Upgrades a single use AuthSub token to a session token.

    Raises:
      NonAuthSubToken if the user's auth token is not an AuthSub token
    """

    if self.__auth_type != AUTHSUB_AUTH_LABEL:
      raise NonAuthSubToken

    (upgrade_connection, uri) = self._PrepareConnection(
        AUTH_SERVER_HOST)
    upgrade_connection.putrequest('GET', '/accounts/AuthSubSessionToken')

    upgrade_connection.putheader('Content-Type',
                                 'application/x-www-form-urlencoded')
    upgrade_connection.putheader('Authorization', '%s=%s' %
        (self.__auth_type, self.__auth_token))
    upgrade_connection.endheaders()

    response = upgrade_connection.getresponse()

    response_body = response.read()
    if response.status == 200:
      for response_line in response_body.splitlines():
        if response_line.startswith('Token='):
          self.__auth_token = response_line.lstrip('Token=')

  def RevokeAuthSubToken(self):
    """Revokes an existing AuthSub token.

    Raises:
      NonAuthSubToken if the user's auth token is not an AuthSub token
    """

    if self.__auth_type != AUTHSUB_AUTH_LABEL:
      raise NonAuthSubToken

    (revoke_connection, uri) = self._PrepareConnection(
        AUTH_SERVER_HOST)
    revoke_connection.putrequest('GET', '/accounts/AuthSubRevokeToken')

    revoke_connection.putheader('Content-Type',
                                'application/x-www-form-urlencoded')
    revoke_connection.putheader('Authorization', '%s=%s' %
            (self.__auth_type, self.__auth_token))
    revoke_connection.endheaders()

    response = revoke_connection.getresponse()
    if response.status == 200:
      self.__auth_type = None
      self.__auth_token = None

  # CRUD operations
  def Get(self, uri, extra_headers=None, redirects_remaining=4, encoding='UTF-8', converter=None):
    """Query the GData API with the given URI

    The uri is the portion of the URI after the server value
    (ex: www.google.com).

    To perform a query against Google Base, set the server to
    'base.google.com' and set the uri to '/base/feeds/...', where ... is
    your query. For example, to find snippets for all digital cameras uri
    should be set to: '/base/feeds/snippets?bq=digital+camera'

    Args:
      uri: string The query in the form of a URI. Example:
           '/base/feeds/snippets?bq=digital+camera'.
      extra_headers: dictionary (optional) Extra HTTP headers to be included
                     in the GET request. These headers are in addition to
                     those stored in the client's additional_headers property.
                     The client automatically sets the Content-Type and
                     Authorization headers.
      redirects_remaining: int (optional) Tracks the number of additional
          redirects this method will allow. If the service object receives
          a redirect and remaining is 0, it will not follow the redirect.
          This was added to avoid infinite redirect loops.
      encoding: string (optional) The character encoding for the server's
          response. Default is UTF-8
      converter: func (optional) A function which will transform
          the server's results before it is returned. Example: use
          GDataFeedFromString to parse the server response as if it
          were a GDataFeed.

    Returns:
      If there is no ResultsTransformer specified in the call, a GDataFeed
      or GDataEntry depending on which is sent from the server. If the
      response is niether a feed or entry and there is no ResultsTransformer,
      return a string. If there is a ResultsTransformer, the returned value
      will be that of the ResultsTransformer function.
    """

    if extra_headers is None:
      extra_headers = {}

    # Add the authentication header to the Get request
    if self.__auth_token:
      extra_headers['Authorization'] = '%s=%s' % (self.__auth_type,
                                                  self.__auth_token)

    if self.__gsessionid is not None:
      if uri.find('gsessionid=') < 0:
        if uri.find('?') > -1:
          uri += '&gsessionid=%s' % (self.__gsessionid,)
        else:
          uri += '?gsessionid=%s' % (self.__gsessionid,)

    server_response = atom.service.AtomService.Get(self, uri, extra_headers)
    result_body = server_response.read()

    if server_response.status == 200:
      if converter:
        return converter(result_body)
      # There was no ResultsTransformer specified, so try to convert the
      # server's response into a GDataFeed.
      feed = gdata.GDataFeedFromString(result_body)
      if not feed:
        # If conversion to a GDataFeed failed, try to convert the server's
        # response to a GDataEntry.
        entry = gdata.GDataEntryFromString(result_body)
        if not entry:
          # The server's response wasn't a feed, or an entry, so return the
          # response body as a string.
          return result_body
        return entry
      return feed
    elif server_response.status == 302:
      if redirects_remaining > 0:
        location = server_response.getheader('Location')
        if location is not None:
          m = re.compile('[\?\&]gsessionid=(\w*)').search(location)
          if m is not None:
            self.__gsessionid = m.group(1)
          return self.Get(location, extra_headers, redirects_remaining - 1,
              encoding=encoding, converter=converter)
        else:
          raise RequestError, {'status': server_response.status,
              'reason': '302 received without Location header',
              'body': result_body}
      else:
        raise RequestError, {'status': server_response.status,
            'reason': 'Redirect received, but redirects_remaining <= 0',
            'body': result_body}
    else:
      raise RequestError, {'status': server_response.status,
          'reason': server_response.reason, 'body': result_body}

  def GetMedia(self, uri, extra_headers=None):
    """Returns a MediaSource containing media and its metadata from the given
    URI string.
    """
    connection = atom.service.AtomService._CreateConnection(self, uri, 'GET',
        extra_headers)
    response_handle = connection.getresponse()
    return gdata.MediaSource(response_handle, response_handle.getheader('Content-Type'),
        response_handle.getheader('Content-Length'))

  def GetEntry(self, uri, extra_headers=None):
    """Query the GData API with the given URI and receive an Entry.

    See also documentation for gdata.service.Get

    Args:
      uri: string The query in the form of a URI. Example:
           '/base/feeds/snippets?bq=digital+camera'.
      extra_headers: dictionary (optional) Extra HTTP headers to be included
                     in the GET request. These headers are in addition to
                     those stored in the client's additional_headers property.
                     The client automatically sets the Content-Type and
                     Authorization headers.

    Returns:
      A GDataEntry built from the XML in the server's response.
    """

    result = self.Get(uri, extra_headers, converter=atom.EntryFromString)
    if isinstance(result, atom.Entry):
      return result
    else:
      raise UnexpectedReturnType, 'Server did not send an entry'

  def GetFeed(self, uri, extra_headers=None,
              converter=gdata.GDataFeedFromString):
    """Query the GData API with the given URI and receive a Feed.

    See also documentation for gdata.service.Get

    Args:
      uri: string The query in the form of a URI. Example:
           '/base/feeds/snippets?bq=digital+camera'.
      extra_headers: dictionary (optional) Extra HTTP headers to be included
                     in the GET request. These headers are in addition to
                     those stored in the client's additional_headers property.
                     The client automatically sets the Content-Type and
                     Authorization headers.

    Returns:
      A GDataFeed built from the XML in the server's response.
    """

    result = self.Get(uri, extra_headers, converter=converter)
    if isinstance(result, atom.Feed):
      return result
    else:
      raise UnexpectedReturnType, 'Server did not send a feed'

  def Post(self, data, uri, extra_headers=None, url_params=None,
           escape_params=True, redirects_remaining=4, media_source=None,
           converter=None):
    """Insert data into a GData service at the given URI.

    Args:
      data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry The
            XML to be sent to the uri.
      uri: string The location (feed) to which the data should be inserted.
           Example: '/base/feeds/items'.
      extra_headers: dict (optional) HTTP headers which are to be included.
                     The client automatically sets the Content-Type,
                     Authorization, and Content-Length headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the URI. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.
      media_source: MediaSource (optional) Container for the media to be sent
          along with the entry, if provided.
      converter: func (optional) A function which will be executed on the
          server's response. Often this is a function like
          GDataEntryFromString which will parse the body of the server's
          response and return a GDataEntry.

    Returns:
      If the post succeeded, this method will return a GDataFeed, GDataEntry,
      or the results of running converter on the server's result body (if
      converter was specified).
    """
    if extra_headers is None:
      extra_headers = {}

    # Add the authentication header to the Get request
    if self.__auth_token:
      extra_headers['Authorization'] = '%s=%s' % (self.__auth_type,
                                                  self.__auth_token)

    if self.__gsessionid is not None:
      if uri.find('gsessionid=') < 0:
        if uri.find('?') > -1:
          uri += '&gsessionid=%s' % (self.__gsessionid,)
        else:
          uri += '?gsessionid=%s' % (self.__gsessionid,)

    if data and media_source:
      if ElementTree.iselement(data):
        data_str = ElementTree.tostring(data)
      else:
        data_str = str(data)

      multipart = []
      multipart.append('Media multipart posting\r\n--END_OF_PART\r\n' + \
          'Content-Type: application/atom+xml\r\n\r\n')
      multipart.append('\r\n--END_OF_PART\r\nContent-Type: ' + \
          media_source.content_type+'\r\n\r\n')
      multipart.append('\r\n--END_OF_PART--\r\n')

      extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART'
      extra_headers['MIME-version'] = '1.0'
      extra_headers['Content-Length'] = str(len(multipart[0]) +
          len(multipart[1]) + len(multipart[2]) +
          len(data_str) + media_source.content_length)

      insert_connection = atom.service.AtomService._CreateConnection(self,
          uri, 'POST', extra_headers, url_params, escape_params)

      insert_connection.send(multipart[0])
      insert_connection.send(data_str)
      insert_connection.send(multipart[1])

      while 1:
        binarydata = media_source.file_handle.read(100000)
        if (binarydata == ""): break
        insert_connection.send(binarydata)

      insert_connection.send(multipart[2])

      server_response = insert_connection.getresponse()
      result_body = server_response.read()

    elif media_source:
      extra_headers['Content-Type'] = media_source.content_type
      extra_headers['Content-Length'] = media_source.content_length
      insert_connection = atom.service.AtomService._CreateConnection(self, uri,
          'POST', extra_headers, url_params, escape_params)

      while 1:
        binarydata = media_source.file_handle.read(100000)
        if (binarydata == ""): break
        insert_connection.send(binarydata)

      server_response = insert_connection.getresponse()
      result_body = server_response.read()

    else:
      http_data = data
      content_type = 'application/atom+xml'

      server_response = atom.service.AtomService.Post(self, http_data, uri,
          extra_headers, url_params, escape_params, content_type)
      result_body = server_response.read()

    # Server returns 201 for most post requests, but when performing a batch
    # request the server responds with a 200 on success.
    if server_response.status == 201 or server_response.status == 200:
      if converter:
        return converter(result_body)
      feed = gdata.GDataFeedFromString(result_body)
      if not feed:
        entry = gdata.GDataEntryFromString(result_body)
        if not entry:
          return result_body
        return entry
      return feed
    elif server_response.status == 302:
      if redirects_remaining > 0:
        location = server_response.getheader('Location')
        if location is not None:
          m = re.compile('[\?\&]gsessionid=(\w*)').search(location)
          if m is not None:
            self.__gsessionid = m.group(1)
          return self.Post(data, location, extra_headers, url_params,
              escape_params, redirects_remaining - 1, media_source,
              converter=converter)
        else:
          raise RequestError, {'status': server_response.status,
              'reason': '302 received without Location header',
              'body': result_body}
      else:
        raise RequestError, {'status': server_response.status,
            'reason': 'Redirect received, but redirects_remaining <= 0',
            'body': result_body}
    else:
      raise RequestError, {'status': server_response.status,
          'reason': server_response.reason, 'body': result_body}

  def Put(self, data, uri, extra_headers=None, url_params=None,
          escape_params=True, redirects_remaining=3, media_source=None,
          converter=None):
    """Updates an entry at the given URI.

    Args:
      data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The
            XML containing the updated data.
      uri: string A URI indicating entry to which the update will be applied.
           Example: '/base/feeds/items/ITEM-ID'
      extra_headers: dict (optional) HTTP headers which are to be included.
                     The client automatically sets the Content-Type,
                     Authorization, and Content-Length headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the URI. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.
      converter: func (optional) A function which will be executed on the
          server's response. Often this is a function like
          GDataEntryFromString which will parse the body of the server's
          response and return a GDataEntry.

    Returns:
      If the put succeeded, this method will return a GDataFeed, GDataEntry,
      or the results of running converter on the server's result body (if
      converter was specified).
    """
    if extra_headers is None:
      extra_headers = {}

    # Add the authentication header to the Get request
    if self.__auth_token:
      extra_headers['Authorization'] = '%s=%s' % (self.__auth_type,
                                                  self.__auth_token)

    if self.__gsessionid is not None:
      if uri.find('gsessionid=') < 0:
        if uri.find('?') > -1:
          uri += '&gsessionid=%s' % (self.__gsessionid,)
        else:
          uri += '?gsessionid=%s' % (self.__gsessionid,)

    if media_source and data:
      if ElementTree.iselement(data):
        data_str = ElementTree.tostring(data)
      else:
        data_str = str(data)

      multipart = []
      multipart.append('Media multipart posting\r\n--END_OF_PART\r\n' + \
          'Content-Type: application/atom+xml\r\n\r\n')
      multipart.append('\r\n--END_OF_PART\r\nContent-Type: ' + \
          media_source.content_type+'\r\n\r\n')
      multipart.append('\r\n--END_OF_PART--\r\n')

      extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART'
      extra_headers['MIME-version'] = '1.0'
      extra_headers['Content-Length'] = str(len(multipart[0]) +
          len(multipart[1]) + len(multipart[2]) +
          len(data_str) + media_source.content_length)

      insert_connection = atom.service.AtomService._CreateConnection(self, uri,
          'PUT', extra_headers, url_params, escape_params)

      insert_connection.send(multipart[0])
      insert_connection.send(data_str)
      insert_connection.send(multipart[1])

      while 1:
        binarydata = media_source.file_handle.read(100000)
        if (binarydata == ""): break
        insert_connection.send(binarydata)

      insert_connection.send(multipart[2])

      server_response = insert_connection.getresponse()
      result_body = server_response.read()

    elif media_source:
      extra_headers['Content-Type'] = media_source.content_type
      extra_headers['Content-Length'] = media_source.content_length
      insert_connection = atom.service.AtomService._CreateConnection(self, uri,
          'PUT', extra_headers, url_params, escape_params)

      while 1:
        binarydata = media_source.file_handle.read(100000)
        if (binarydata == ""): break
        insert_connection.send(binarydata)

      server_response = insert_connection.getresponse()
      result_body = server_response.read()
    else:
      http_data = data
      content_type = 'application/atom+xml'

      server_response = atom.service.AtomService.Put(self, http_data, uri,
          extra_headers, url_params, escape_params, content_type)
      result_body = server_response.read()

    if server_response.status == 200:
      if converter:
        return converter(result_body)
      feed = gdata.GDataFeedFromString(result_body)
      if not feed:
        entry = gdata.GDataEntryFromString(result_body)
        if not entry:
          return result_body
        return entry
      return feed
    elif server_response.status == 302:
      if redirects_remaining > 0:
        location = server_response.getheader('Location')
        if location is not None:
          m = re.compile('[\?\&]gsessionid=(\w*)').search(location)
          if m is not None:
            self.__gsessionid = m.group(1)
          return self.Put(data, location, extra_headers, url_params,
              escape_params, redirects_remaining - 1,
              media_source=media_source, converter=converter)
        else:
          raise RequestError, {'status': server_response.status,
              'reason': '302 received without Location header',
              'body': result_body}
      else:
        raise RequestError, {'status': server_response.status,
            'reason': 'Redirect received, but redirects_remaining <= 0',
            'body': result_body}
    else:
      raise RequestError, {'status': server_response.status,
          'reason': server_response.reason, 'body': result_body}

  def Delete(self, uri, extra_headers=None, url_params=None,
             escape_params=True, redirects_remaining=4):
    """Deletes the entry at the given URI.

    Args:
      uri: string The URI of the entry to be deleted. Example:
           '/base/feeds/items/ITEM-ID'
      extra_headers: dict (optional) HTTP headers which are to be included.
                     The client automatically sets the Content-Type and
                     Authorization headers.
      url_params: dict (optional) Additional URL parameters to be included
                  in the URI. These are translated into query arguments
                  in the form '&dict_key=value&...'.
                  Example: {'max-results': '250'} becomes &max-results=250
      escape_params: boolean (optional) If false, the calling code has already
                     ensured that the query will form a valid URL (all
                     reserved characters have been escaped). If true, this
                     method will escape the query and any URL parameters
                     provided.

    Returns:
      True if the entry was deleted.
    """
    if extra_headers is None:
      extra_headers = {}

    # Add the authentication header to the Get request
    if self.__auth_token:
      extra_headers['Authorization'] = '%s=%s' % (self.__auth_type,
                                                  self.__auth_token)

    if self.__gsessionid is not None:
      if uri.find('gsessionid=') < 0:
        if uri.find('?') > -1:
          uri += '&gsessionid=%s' % (self.__gsessionid,)
        else:
          uri += '?gsessionid=%s' % (self.__gsessionid,)

    server_response = atom.service.AtomService.Delete(self, uri,
        extra_headers, url_params, escape_params)
    result_body = server_response.read()

    if server_response.status == 200:
      return True
    elif server_response.status == 302:
      if redirects_remaining > 0:
        location = server_response.getheader('Location')
        if location is not None:
          m = re.compile('[\?\&]gsessionid=(\w*)').search(location)
          if m is not None:
            self.__gsessionid = m.group(1)
          return self.Delete(location, extra_headers, url_params,
              escape_params, redirects_remaining - 1)
        else:
          raise RequestError, {'status': server_response.status,
              'reason': '302 received without Location header',
              'body': result_body}
      else:
        raise RequestError, {'status': server_response.status,
            'reason': 'Redirect received, but redirects_remaining <= 0',
            'body': result_body}
    else:
      raise RequestError, {'status': server_response.status,
          'reason': server_response.reason, 'body': result_body}

class Query(dict):
  """Constructs a query URL to be used in GET requests

  Url parameters are created by adding key-value pairs to this object as a
  dict. For example, to add &max-results=25 to the URL do
  my_query['max-results'] = 25

  Category queries are created by adding category strings to the categories
  member. All items in the categories list will be concatenated with the /
  symbol (symbolizing a category x AND y restriction). If you would like to OR
  2 categories, append them as one string with a | between the categories.
  For example, do query.categories.append('Fritz|Laurie') to create a query
  like this feed/-/Fritz%7CLaurie . This query will look for results in both
  categories.
  """

  def __init__(self, feed=None, text_query=None, params=None,
      categories=None):
    """Constructor for Query

    Args:
      feed: str (optional) The path for the feed (Examples:
          '/base/feeds/snippets' or 'calendar/feeds/jo@gmail.com/private/full'
      text_query: str (optional) The contents of the q query parameter. The
          contents of the text_query are URL escaped upon conversion to a URI.
      params: dict (optional) Parameter value string pairs which become URL
          params when translated to a URI. These parameters are added to the
          query's items (key-value pairs).
      categories: list (optional) List of category strings which should be
          included as query categories. See
          http://code.google.com/apis/gdata/reference.html#Queries for
          details. If you want to get results from category A or B (both
          categories), specify a single list item 'A|B'.
    """

    self.feed = feed
    self.categories = []
    if text_query:
      self.text_query = text_query
    if isinstance(params, dict):
      for param in params:
        self[param] = params[param]
    if isinstance(categories, list):
      for category in categories:
        self.categories.append(category)

  def _GetTextQuery(self):
    if 'q' in self.keys():
      return self['q']
    else:
      return None

  def _SetTextQuery(self, query):
    self['q'] = query

  text_query = property(_GetTextQuery, _SetTextQuery,
      doc="""The feed query's q parameter""")

  def _GetAuthor(self):
    if 'author' in self.keys():
      return self['author']
    else:
      return None

  def _SetAuthor(self, query):
    self['author'] = query

  author = property(_GetAuthor, _SetAuthor,
      doc="""The feed query's author parameter""")

  def _GetAlt(self):
    if 'alt' in self.keys():
      return self['alt']
    else:
      return None

  def _SetAlt(self, query):
    self['alt'] = query

  alt = property(_GetAlt, _SetAlt,
      doc="""The feed query's alt parameter""")

  def _GetUpdatedMin(self):
    if 'updated-min' in self.keys():
      return self['updated-min']
    else:
      return None

  def _SetUpdatedMin(self, query):
    self['updated-min'] = query

  updated_min = property(_GetUpdatedMin, _SetUpdatedMin,
      doc="""The feed query's updated-min parameter""")

  def _GetUpdatedMax(self):
    if 'updated-max' in self.keys():
      return self['updated-max']
    else:
      return None

  def _SetUpdatedMax(self, query):
    self['updated-max'] = query

  updated_max = property(_GetUpdatedMax, _SetUpdatedMax,
      doc="""The feed query's updated-max parameter""")

  def _GetPublishedMin(self):
    if 'published-min' in self.keys():
      return self['published-min']
    else:
      return None

  def _SetPublishedMin(self, query):
    self['published-min'] = query

  published_min = property(_GetPublishedMin, _SetPublishedMin,
      doc="""The feed query's published-min parameter""")

  def _GetPublishedMax(self):
    if 'published-max' in self.keys():
      return self['published-max']
    else:
      return None

  def _SetPublishedMax(self, query):
    self['published-max'] = query

  published_max = property(_GetPublishedMax, _SetPublishedMax,
      doc="""The feed query's published-max parameter""")

  def _GetStartIndex(self):
    if 'start-index' in self.keys():
      return self['start-index']
    else:
      return None

  def _SetStartIndex(self, query):
    if not isinstance(query, str):
      query = str(query)
    self['start-index'] = query

  start_index = property(_GetStartIndex, _SetStartIndex,
      doc="""The feed query's start-index parameter""")

  def _GetMaxResults(self):
    if 'max-results' in self.keys():
      return self['max-results']
    else:
      return None

  def _SetMaxResults(self, query):
    if not isinstance(query, str):
      query = str(query)
    self['max-results'] = query

  max_results = property(_GetMaxResults, _SetMaxResults,
      doc="""The feed query's max-results parameter""")

  def ToUri(self):
    q_feed = self.feed or ''
    category_string = '/'.join(
        [urllib.quote_plus(c) for c in self.categories])
    # Add categories to the feed if there are any.
    if len(self.categories) > 0:
      q_feed = q_feed + '/-/' + category_string
    return atom.service.BuildUri(q_feed, self)

redpajama_set_name: RedPajamaGithub


Now, who would have expected me to be quoting from a Shakespeare Sonnet (No 18 to be precise) in a Little Dragon's Journal entry? Certainly, not me!
The sonnet is the one that begins – 'Shall I compare thee to a summer's day?' and goes on to speak of 'the darling buds of May' , which are shaken by rough winds.
For some unknown reason, this phrase sprang into my head when I was watching several Little Dragons enjoying their time with us during a recent Tuesday morning session.
Wow, and I thought that these small children and babies were simply enjoying the moment as they skipped, hopped, rode the bicycles and the famous red car, crawled hither and thither, or simply sat happily smiling in the Children's Corner!
And yet, and yet! Yes, indeed, they are reaching forward to growing and developing into the adults they will eventually become.
If only we could see ourselves as we were when we were very young. Many who are reading this article will have been babies well before the time of video cameras and smart phones. We may have photographs (some of them possibly in shades of sepia) that give an idea of how we may have appeared to the rest of our small world, but a view of ourselves as living, moving, talking beings may have to be left entirely to the imagination.
I know that other Dragon helpers feel as I do in that we enjoy a tremendous privilege as we view the growth and development of our Little Dragon friends, as we may perhaps, indeed, see them as opening buds that point toward the warm summer season ahead. It is my dearest wish that they, and all the other youngsters in our world, will grow to live in a safe and caring world; a world without strife, envy and inhumanity. If we are prayerful, maybe we can pray for their future world and hope for a world where peace exists, where food and water is plentiful, where health issues are dealt with; a world where people of different races and cultures live together with love and respect for their fellow humans.


text:
Q: Interpolate between two colours based on a percentage value I'm currently developing an animation library in JS and need to calculate a colour between two values based on a progress value between 0 - 1.
For example, the function might look something below. This would input two colours in HEX format.

*

*colourA = Inital Colour

*colourB = Final Colour

*progress = 0.5 (50%)

const interpolateColour = (colourA, colourB, progress) => { return polColour }

The goal is to return a colour 0.5 progress or 50% between the two colours. I know HEX colour would most likely need to be converted to RGBA for HSV to accomplish this effect, although unsure which is the best approach.
EDIT:
I figured it out...

const is = {
    hex: a => /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a),
    rgb: a => /^rgb/.test(a),
    hsl: a => /^hsl/.test(a),
    col: a => (is.hex(a) || is.rgb(a) || is.hsl(a)),
}

const convertToRgba = (colour) => {
    return is.hex(colour) ? hexToRgba(colour)
        : is.rgb(colour) ? rbgToRgba(colour)
            : is.hsl(colour) ? hslToRgba(colour)
                : colour
}

const hexToRgba = (colour, alpha = 1) => {
    const [r, g, b] = colour.match(/\w\w/g).map(x => parseInt(x, 16))
    return `rgba(${r},${g},${b},${alpha})`
};

const rbgToRgba = (colour, alpha = 1) => {
    const [r, g, b] = colour.replace(/[^\d,]/g, '').split(',')
    return `rgba(${r},${g},${b},${alpha})`
}

const deconstructRgba = (rgba) => {
    return rgba.replace(/[^\d,]/g, '').split(',').map(x => parseInt(x))
}

const formatRbga = (colour) => {
    return `rgba(${colour.r},${colour.g},${colour.b},${colour.a})`
}

const interpolateColour = (colourA, colourB, progress) => {
    const [r1, g1, b1, a1] = deconstructRgba(convertToRgba(colourA))
    const [r2, g2, b2, a2] = deconstructRgba(convertToRgba(colourB))
    return formatRbga({
        r: Math.round((r1 + r2) * progress),
        g: Math.round((g1 + g2) * progress),
        b: Math.round((b1 + b2) * progress),
        a: Math.round((a1 + a2) * progress)
    })
}

export {
    interpolateColour,
    convertToRgba,
    hexToRgba,
    rbgToRgba,
    deconstructRgba
}

A: A simple interpolation of the R, G, and B values should suffice.

// extract numeric r, g, b values from `rgb(nn, nn, nn)` string
function getRgb(color) {
  let [r, g, b] = color.replace('rgb(', '')
    .replace(')', '')
    .split(',')
    .map(str => Number(str));;
  return {
    r,
    g,
    b
  }
}

function colorInterpolate(colorA, colorB, intval) {
  const rgbA = getRgb(colorA),
    rgbB = getRgb(colorB);
  const colorVal = (prop) =>
    Math.round(rgbA[prop] * (1 - intval) + rgbB[prop] * intval);
  return {
    r: colorVal('r'),
    g: colorVal('g'),
    b: colorVal('b'),
  }
}

function doit(progression) {
  const div1 = document.getElementById('color1');
  const color1 = div1.style.backgroundColor;
  const div2 = document.getElementById('color2');
  const color2 = div2.style.backgroundColor;

  const rgbNew = colorInterpolate(
    color1,
    color2, progression
  );

  const divResult = document.getElementById('result');
  divResult.style.backgroundColor =
    `rgb( ${rgbNew.r}, ${rgbNew.g}, ${rgbNew.b})`;
}

document.querySelector('button').onclick = () => {
  const intval = Number(document.querySelector('input').value);
  doit(intval);
};
#color1,
#color2,
#result {
  width: 200px;
  height: 40px;
  margin: 12px;
  padding: 0.2rem 0.5rem;
}

Interpolate Between Two Colors

Interpolate by:
<input id="number" type="number" value="0.5" min="0" max="1" step="0.05" />
<button>Interpolate</button>

color1

interpolated

color2

A: Using HEX colours:
function convert(col1, col2, p) {
  const rgb1 = parseInt(col1, 16);
  const rgb2 = parseInt(col2, 16);

  const [r1, g1, b1] = toArray(rgb1);
  const [r2, g2, b2] = toArray(rgb2);

  const q = 1-p;
  const rr = Math.round(r1 * p + r2 * q);
  const rg = Math.round(g1 * p + g2 * q);
  const rb = Math.round(b1 * p + b2 * q);

  return  Number((rr << 16) + (rg << 8) + rb).toString(16);
}

function toArray(rgb) {
  const r = rgb >> 16;
  const g = (rgb >> 8) % 256;
  const b = rgb % 256;

  return [r, g, b];
}

redpajama_set_name: RedPajamaStackExchange


text:
Which Members of Congress Are On Twitter?
Nick Baumann
Which members of Congress are on Twitter? A lot of them, it turns out. A new report (PDF) by the Congressional Research Service examines the use of Twitter by representatives and senators during August and September of 2009:
The data show that 205 Representatives and Senators are registered with Twitter (as of September 30, 2009) and issued a total of 7,078 "tweets" during the data collection period of August and September 2009. With approximately 38% of House Members and 39% of Senators registered with Twitter, Members sent an average of 116 tweets per day collectively.
Unsurprisingly, member use of Twitter mirrors that of regular people: many registered members don't tweet at all, while a few members are responsible for a big chunk of the tweets. Half of the members of Congress on Twitter sent 20 or fewer tweets during August and September. But "16 Members sent more than 100 tweets," according to the report. Members of the House tweet far more often than Senators, and House Republicans are far more active tweeters than House Democrats. (Senate Republicans tweet slightly more frequently than Senate Democrats.)
The most interesting part of the CRS report focuses on the content of members' tweets. The authors of the study classified all the tweets they looked at into eight categories: position taking, policy statements, media or public relations, district or state, official or congressional action, personal, campaign, and other. Bottom line?
At this time, Twitter largely facilitates a one-way transmission of information from Members to the public. Members use Twitter to convey information about their official actions, press appearances, or policy positions. Given the limited data available thus far, a two-way exchange of information or policy dialogue appears less frequent.
That's also unsurprising, but it's too bad. It would be good for everyone if more Americans thought their representatives were hearing their concerns. Anyway, one more item. While the median representative had 1,297 followers, and the median senator had 3,536, one senator is the undisputed king of congressional twitterers: Sen. John McCain. He has over 1.7 million.
(h/t: Steven Aftergood)
Are Twitter and Facebook Bad for You?
Nikki Gloudeman
Congress Atwitter Over Members' Use of Social Networking, Video Sites
Bruce Falconer

redpajama_set_name: RedPajamaCommonCrawl


text:
S. Narayan
The Dravidian Years: Politics and Welfare in Tamil Nadu
Versandfertig in über 4 Wochen
An insider's view of fifty years of rule in Tamil Nadu by the DMK and the AIADMK, Dravidian parties that started off on a platform of social justice, social reform, and empowerment, and have now morphed into political machines using social welfare labels as political tools for electoral victories. The author was an IAS officer of the Tamil Nadu cadre who witnessed the social changes brought about by successive governments. The book narrates the sequence of events insuccessive administrations to implement this agenda, and argues that a combination of people, politics, and administrative efficiency made this possible.…mehr
Ahmed S. Hashim
When Counterinsurgency Wins: Sri Lanka's Defeat of the Tamil Tigers
V. Ravi Vaithees
Religion, Caste, and Nation in South India: Maraimalai Adigal, the Neo-Saivite Movement, and Tamil Nationalism, 1876-1950
K. M. A. Ahamed Zubair
Islamic Mysticism in Tamil Nadu
Kate Kenski
The Obama Victory: How Media, Money, and Message Shaped the 2008 Election
Robert E. Mutch
Buying the Vote: A History of Campaign Finance Reform
Edward Jewitt Robinson
Tales and Poems of South India: From the Tamil
Four Hats in the Ring: The 1912 Election and the Birth of Modern American Politics
An insider's view of fifty years of rule in Tamil Nadu by the DMK and the AIADMK, Dravidian parties that started off on a platform of social justice, social reform, and empowerment, and have now morphed into political machines using social welfare labels as political tools for electoral victories. The author was an IAS officer of the Tamil Nadu cadre who witnessed the social changes brought about by successive governments. The book narrates the sequence of events insuccessive administrations to implement this agenda, and argues that a combination of people, politics, and administrative efficiency made this possible.
Verlag: OXFORD UNIV PR
Erscheinungstermin: 26. September 2018


Last night I offered to watch my parents' dog while they go out of town for a few days. At 4 a.m. I began to rethink my plan.
I love pets, especially cats and dogs. I've grown up with family pets, and this little guy is one of them. Paco was a Christmas gift for my step-mom in 2000. He's an awesome dog. One of my all-time favorite family pets. He's half chihuahua and maltese, and has the sweetest personality.
So it seemed like a great idea to just bring him home, and let him stay with me for a few days. I thought our biggest hurdle would be my two kitties, Georgie and Tammy, who out-weigh him by a couple of pounds. The cats were surprisingly docile and mostly just avoided him.
My terror set in at 4 a.m. I got up to go to the bathroom and let Paco go potty. I realized that I waited a little too long, when I felt moisture on the rug. He had gone to the back door, and couldn't hold it any longer.
Since the backyard is fenced, I opened the door and let him out to hopefully do some more business in the grass. I left the door cracked open, and I put the rug in the washing machine.
When I peeked back outside, he wasn't there. I grabbed a coat and flashlight, and searched the backyard. I must have made five panicked laps around the yard. I looked under hedges and in flower beds. I just knew I would see his little white body hiding in the brush. Yet, no Paco.
After calling for him while making several laps around the yard, and probably waking up a few neighbors, the little guy was discovered across the street sniffing around in someone's open garage. He was on a great adventure and in no hurry to come back home.
Thankfully once inside he settled in and went back to sleep, unlike myself and the two cats. We were up for another hour. This morning his escape route was discovered, and he gets to the spend the day supervised by the boyfriend. If I'm lucky, we'll have less grand adventures this weekend.


text:
← Archive Spotlight: Charity Circle of Murfreesboro
Tennesseans Respond: Brown v. Board of Education →
Albert Gore Research Center's 25th Anniversary
Posted on April 8, 2019	by Albert Gore Research Center
Written by Sarah Calise, Archivist
As we wrap up the 2018-2019 academic year, the Albert Gore Research Center's 25th anniversary celebrations will also come to a close. Starting today through the beginning of May, we will post 25 highlights from our collections across our social media platforms, specifically Facebook and Twitter. Look for the hashtag #GoreCenter25. Additionally, Director Dr. Louis Kyriakoudes and Gore Center staff invite the public to our Open House this Friday, April 12 from 9AM-4PM. We will display highlights from our collection in our research room, we will conduct small group tours of our archival storage, we will provide information on how to donate to our archive, and there will be some refreshments in our conference room from 1:00-3:00PM, so come join us!
Senator Albert Gore, Sr. speaks passionately before an audience.
How did the Albert Gore Research Center get its start? The answer is in our name! Middle Tennessee State University acquired the congressional papers of Albert Gore, Sr. (MTSU Class of 1932) following his retirement from politics in 1970. Two professors from the Department of Political Science, Dr. Norman L. Parks and Dr. David Grubbs, were instrumental in persuading Gore to donate his papers to his alma mater. The papers first found a home with Dr. Jim Neal of the History Department, who started the archiving process. In 1992, the papers moved to the Learning Resources Center (LRC), where the Albert Gore Research Center began opening its doors to the public in February 1993 under the leadership of Dr. Neal as the first director. During this time, the Gore Center began expanding its collections to include university and regional history. Space and storage issues pushed the Gore Center out of the Learning Resources Center and into the newly renovated Andrew L. Todd Hall in the spring of 2005. Dr. Lisa Pruitt, the director after Dr. Neal retired in 1999, oversaw the move to the new space.
Since moving to Todd Hall, the Gore Center's collections and outreach initiatives have grown richer and more expansive. Our oral history collections greatly increased over the years, and we currently preserve thousands of audio files that cover stories of Tennesseans from all walks of life. Betty Rowland, former Gore Center secretary, was a crucial figure in collecting oral histories for the Middle Tennessee Oral History Project. She conducted over 200 interviews between the years 2000 and 2004. In 2003, Don and Sheryl Jones donated some of the oldest-known photographs of Murfreesboro. These photographs are dated between 1865 and 1870, and show vestiges of the Union encampment on the town's Public Square.
Union encampment on Murfreesboro square, ca. 1860s. Don and Sheryl Jones Photograph Collection.
The third Gore Center director, Dr. Jim Williams, called 2010 a "transformative year" for our archive for a number of a reasons. Additional compact shelving units were added to the storage area, which came in handy when U.S. Representative Bart Gordon donated his congressional papers to the Gore Center following his retirement from politics. In total, we received over 600 cartons of papers and an electronic database from Gordon. That same year, we acquired another large donation to our collections, this one specifically for the Margaret Lindsley Warden Library for Equine Studies. Joan Hunt donated over 900 books and 5,500 magazines. Today, the Library for Equine Studies is housed in the Special Collections at James E. Walker Library. Lastly, in 2010, the Gore Center participated in the planning for MTSU's Centennial Celebration that occurred the following year.
Perhaps the most important event that happened around this time was the hiring of Donna Baker, MTSU's first University Archivist. She has been an instrumental figure in collecting and preserving MTSU's history, and her knowledge and leadership has been vital to the Gore Center's daily operations and public history training. Donna trained me as a graduate student from 2014 to 2016, and I am eternally grateful for her mentorship in these early years of my professional career.
An undergraduate history class discusses what they have found in some of the Gore Center's archival documents.
Since 2014, the Gore Center has continued its dedication to providing the public and MTSU students with access to our many political, regional, university, and oral history collections. We have opened up our doors to classes across MTSU's campus for hands-on primary source learning, and we have taken the archives into the classrooms of local Murfreesboro public schools. Our graduate assistants have developed countless physical and online exhibits using our collections. We also started a partnership with Walker Library to create and manage more digital collections, including the Forrest Hall Protest Collection. We have built relationships with communities and institutions across the state of Tennessee, like Humanities Tennessee and MTSU's Teaching with Primary Sources. We continue to sponsor public programs such as Congress to Campus, and we have developed successful programs of our own, like the Movement 68 Symposium held in October 2018.
Participants in the Movement 68 Symposium: Honoring 50 Years of Black Student Activism, left to right: Sarah Calise (moderator), André Canty, Arionna White, Sylvester Brooks, Dr. Michael McDonald, Dr. Phyllis Hickerson-Washington, Dr. Vincent Windrow, Lae'l Hughes-Watkins (keynote speaker), and Barbara Scales (moderator).
We can't know for sure what kind of exciting adventures await the Albert Gore Research Center and the archives world in the future, but we do know that our archivists and archivists-in-training will be here when you need us–for your next research paper, class assignment, exhibit, documentary, oral history, public program, community project, or even if you are just looking for some cool historical documents and artifacts. We have been honored to serve the Middle Tennessee community for the past 25 years, and we cannot wait to serve the public for 25 more years and beyond!
Visit our website or contact us to start your next project with the Gore Center.
This entry was posted in collections, Events, exhibits, Oral History, political collections, regional, university and tagged about us, Albert Gore Sr., anniversary, Bart Gordon, Gore Center 25. Bookmark the permalink.


Rich Restucci is a practicing chemist living in Pembroke Massachusetts. He resides with his lovely wife, three children, and a permanent hangover. He enjoys drinking beer, stocking up on weapons and supplies, playing with explosives and reading/writing anything zombie related. An up and coming writer, Rich has been fortunate to have a few stories published, one in the anthology Dead Worlds 7, two in the anthology Feast or Famine: A Banquet of Tales for the Zombie Prepper, and his first novel, Run is also now available. Rich is currently working on publishing several other stories and a few novels.


text:
package net.sourceforge.jeval;

import net.sourceforge.jeval.operator.Operator;

/**
 * Represents an operator being processed in the expression.
 */
public class ExpressionOperator {

	// The operator that this object represents.
	private Operator operator = null;

	// The unary operator for this object, if there is one.
	private Operator unaryOperator = null;

	/**
	 * Creates a new ExpressionOperator.
	 *
	 * @param operator
	 *            The operator this object represents.
	 * @param unaryOperator
	 *            The unary operator for this object.
	 */
	public ExpressionOperator(final Operator operator,
			final Operator unaryOperator) {
		this.operator = operator;
		this.unaryOperator = unaryOperator;
	}

	/**
	 * Returns the operator for this object.
	 *
	 * @return The operator for this object.
	 */
	public Operator getOperator() {
		return operator;
	}

	/**
	 * Returns the unary operator for this object.
	 *
	 * @return The unary operator for this object.
	 */
	public Operator getUnaryOperator() {
		return unaryOperator;
	}
}


text:
Frank Eisenhauer (* 9. Juni 1968 in Augsburg) ist ein deutscher Astronom und Astrophysiker. Bekannt ist er vor allem für seine Beiträge zur Interferometrie und Spektroskopie sowie der Erforschung des Schwarzen Lochs im Zentrum der Milchstraße.

Leben
Frank Eisenhauer ist in Augsburg aufgewachsen und erlangte 1987 sein Abitur am Justus-von-Liebig Gymnasium in Neusäß. Den Wehrdienst leistete er beim Gebirgsfernmeldebataillon 8 in Murnau. Eisenhauer ist verheiratet, hat drei Kinder und lebt in München.

Studium
Frank Eisenhauer studierte Physik an der Technischen Universität München (1988–1995) und ist seit seiner Diplomarbeit (1995) bis heute am Max-Planck-Institut für extraterrestrische Physik (MPE) tätig. Dort verfasste er bei Reinhard Genzel seine Doktorarbeit, mit der er 1998 an der Ludwig-Maximilian-Universität München promovierte. 2011 habilitierte Eisenhauer an der Technischen Universität München.

Lehrtätigkeit
Frank Eisenhauer lehrt als außerplanmäßiger Professor an der Technischen Universität München Astrophysik und hochauflösende Astronomie.

Wissenschaft und Forschung
Als Leiter (Principal Investigator) von zwei großen astrophysikalischen Experimenten hat Frank Eisenhauer maßgeblich die Entwicklung der Astronomie mit höchster räumlicher Auflösung und der abbildenden Spektroskopie vorangetrieben, und damit insbesondere zur Entdeckung und Erforschung des Schwarzen Loch im Zentrum der Milchstraße beigetragen. Am MPE leitet er heute die Entwicklung und wissenschaftliche Auswertung großer astronomischer Instrumente und Experimente.

Bereits in seiner Doktorarbeit hat sich Eisenhauer der Infrarotastronomie zugewandt und für die adaptive Optik am 3.6m Teleskop der Europäischen Südsternwarte (ESO) in La Silla (Chile) eine Infrarotkamera mit Fabry-Pérot-Spektrometer entwickelt. Anschließend hat er als Principal Investigator die Entwicklung des SPIFFI/SINFONI Spektrometers am ESO Very Large Telescope in Paranal (Chile) geleitet, das mit einer damals einzigartigen Kombination aus adaptiver Optik und abbildender Spektroskopie nicht nur die Störung durch die Erdatmosphäre korrigiert, sondern gleichzeitig für jeden Bildpunkt der Aufnahme ein Spektrum aufzeichnet. Damit konnten Eisenhauer und Kollegen 2003 erstmals mit geometrischen Methoden aus der Umlaufbahn des Sterns S2 die Entfernung zum Zentrum der Milchstraße messen, und mit der Vermessung der Radialgeschwindigkeiten mehrerer Sterne die Vermutung bekräftigen, dass sich dort ein supermassives Schwarzes Loch befindet.

Seit 2005 ist Frank Eisenhauer Principal Investigator des GRAVITY Experiments, mit dem die vier Very Large Telescope in Paranal (Chile) der Europäischen Südsternwarte als Stern-Interferometer zusammengeschaltet werden und damit eine Winkelauflösung erreichen, die dem eines Teleskopes mit 130 Meter Durchmesser entspricht. Ähnlich wie bei einer adaptiven Optik werden bei GRAVITY die störenden Einflüsse der Erdatmosphäre und die Störungen im Lichtweg zwischen Teleskop und Labor aktiv korrigiert, wodurch die Empfindlichkeit um mehrere Größenordnungen im Vergleich zu früheren Experimenten verbessert wurde. Damit konnten Frank Eisenhauer und Kollegen 2018 insbesondere die aus der Allgemeinen Relativitätstheorie von Albert Einstein vorausgesagte Rotverschiebung im Schwerefeld eines schwarzen Loches nachweisen. Dem gleichen Team gelang im Jahr 2020 auch der Nachweis der Schwarzschild-Präzession (Orbit-Vergleich Newton und Einstein) im Orbit des Sterns S2.
Die geometrische Messung der Entfernung zum Galaktischen Zentrum und der Nachweis der Gravitationsrotverschiebung im Schwerefeld des schwarzen Loches wurden von Andrea Ghez und Kollegen mit Beobachtungen am Keck-Observatorium auf Hawaii bestätigt.

SINFONI und GRAVITY gehören zu den Instrumenten, die bei der Entdeckung und Charakterisierung des Schwarzen Lochs im Galaktischen Zentrum eingesetzt wurden, wofür Reinhard Genzel und Andrea Ghez den Nobelpreis für Physik 2020 erhalten haben.

Weitere Forschungsfelder, zu denen Frank Eisenhauer mit seinen Beobachtungen unter anderem beigetragen hat, sind die Galaxiendynamik im frühen Universum, Aktive Galaxienkerne, und die Sternentstehung in massereichen Sternhaufen.

Ehrungen und Auszeichnungen

 Gruber-Preis für Kosmologie 2022 für die Entwicklung von Instrumenten, die unwiderlegbare Hinweise auf die Existenz eines Schwarzen Lochs im Zentrum unserer Galaxie gesammelt haben
 Stern-Gerlach-Medaille 2022 der Deutsche Physikalische Gesellschaft für seine Pionierarbeit auf dem Gebiet der hochauflösenden Infrarotastronomie
 Jackson-Gwilt-Medaille 2022 der Royal Astronomical Society für seine Entwicklung von astronomischen Instrumenten
 Ausländisches Mitglied der Französische Akademie der Wissenschaften in 2021
 Tycho-Brahe-Preis 2021 der European Astronomical Society für die Entwicklung der SINFONI and GRAVITY Instrumente
 Michelson Investigator Achievement Award 2020 für die bahnbrechende Ergebnisse von VLTI-GRAVITY

Mitgliedschaften
 International Astronomical Union, Paris
 Astronomische Gesellschaft, Heidelberg
 European Astronomical Society (EAS), Genf

Weblinks
 Publikationen von F. Eisenhauer im Astrophysics Data System, abgerufen am 2. Oktober 2019
 https://www.eso.org/public/germany/news/eso1825/?lang Pressekonferenz zum Nachweis der Rotverschiebung im Schwerefeld des Schwarzen Loches, abgerufen am 2. Oktober 2019
 https://www.eso.org/public/germany/news/eso1601/?lang Pressemitteilung zur Inbetriebnahme von SPIFFI/SINFONI und GRAVITY, abgerufen am 2. Oktober 2019
 http://hyperraum.tv/2017/06/14/die-beobachtung-des-unbeobachtbaren/  Interview und Allgemeinverständliche Erklärung zum Schwarzen Loch im Zentrum der Milchstraße und zu GRAVITY, abgerufen am 2. Oktober 2019
 http://www.mpe.mpg.de/~eisenhau/ Profil Frank Eisenhauer bei Max-Planck-Institut für extraterrestrische Physik, Garching, abgerufen am 2. Oktober 2019
 https://www.ph.tum.de/about/people/vcard/DB021172F366B24F Profil Frank Eisenhauer bei Technischer Universität München, abgerufen am 2. Oktober 2019
 https://www.eso.org/public/germany/news/eso2006/?lang Pressemitteilung zum Nachweis der Schwarzschild-Präzession im Orbit des Sterns S2 um das Schwarze Loch im Zentrum der Milchstraße, abgerufen am 24. April 2020

Einzelnachweise

Physiker (20. Jahrhundert)
Astronom (20. Jahrhundert)
Physiker (21. Jahrhundert)
Astronom (21. Jahrhundert)
Astrophysiker
Sachbuchautor (Astronomie)
Deutscher
Geboren 1968
Mann
Sachbuchautor (Astrophysik)

redpajama_set_name: RedPajamaWikipedia


KNOXVILLE, Tenn. (Sept. 14, 2018) — Aisle Ahead, Inc. announced today it will attend the 2018 Path to Purchase Expo being held at the Minneapolis Convention Center in Minneapolis, MN October 2-4, 2018.
Aisle Ahead is a solutions provider for grocery retailers, GPG brands, and smart kitchen appliance manufacturers to enhance the consumer grocery shopping journey. Aisle Ahead's solutions are custom-developed and designed to compete with the rapidly changing world of grocery and meet growing shopper expectations by providing personalized digital experiences.
The Path to Purchase Expo includes attendees such as Kroger, Walmart, and Coca-Cola and exhibitors of industry partners and solutions providers, as well as a full agenda of networking opportunities, presentations, and seminars throughout the duration of the event.
Headquartered in Knoxville, Tenn., Aisle Ahead, Inc. delivers a proven digital commerce platform paired with sourced content, enabling grocers, brands, developers, and individuals to connect and interact with personalized shopping experiences. For more information visit aisleahead.com, bigoven.com.
Aisle Ahead is attending Groceryshop in Las Vegas!


text:
Q: React.js - how to hit enter without creating an enter space (paragraph space)? When you hit enter a new filed will be created but the problem is the focus and the focus will move to the newly created filed, but the problem is : its leave an enter space in the previous field
`

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  let elements = [
    { id: 0, text: "first", autoFocus: true },
    { id: 1, text: "second", autoFocus: true },
    { id: 2, text: "third", autoFocus: true }
  ];
  const [state, setstate] = useState(elements);
  function handChange(e) {
    if (e.keyCode == 13) {
      const index = state.findIndex((item) => item.id == e.target.id);
      //i'm using autoFocus to move the focus  (I-beam pointer) to the nex field.
      //but i still get errors with it
      Object.assign(state, (state[index].autoFocus = false));

      setstate((pre) => {
        return [
          ...pre.slice(0, index + 1),
          { id: 3, text: "xxx", autoFocus: true },
          ...pre.slice(index + 1, state.length)
        ];
      });
      setTimeout(() => {
        document.getElementById(index + 1).focus();
      }, 0);
    }
  }

  return (

      {state.map((e) => (

          {e.text}

      ))}

  );
}

on codesandbox

A: The focus attribute will not work on a div element.
You should use an input element with the autoFocus attribute in React.
<input autoFocus={e.autoFocus} value={e.text} id={e.id} onKeyUp={handChange}/>

A: Try out to clone the state then map it by updating the autoFocus and text properties :
 function handChange(e) {
    if (e.keyCode === 13) {
      let _state = [...state];

      setstate(
        _state.map((item) => {
          if (+e.target.id === +item.id) {
            return {
              ...item,
              autoFocus: false
            };
          } else if (+e.target.id + 1 === +item.id + 1) {
            return {
              ...item,
              autoFocus: true,
              text: "xxx"
            };
          } else {
            return item;
          }
        })
      );
    }
  }

  return (

      {state.map((e) => (
        <input
          focus={e.autoFocus}
          contentEditable="true"
          id={e.id}
          onKeyUp={handChange}
          value={e.text}
        />
      ))}

  );


This Oil Cloth tote is the perfect tote for all your portable needs. Cheerful pink deign pattern and sturdy white cotton straps. Measures 12 inches high by 15 inches wide. Waterproof oil cloth is great for keeping your items dry on these wet winter days.


text:
The Swangla is a tribal community found in Lahaul and Spiti district of Himachal Pradesh, India. They are primarily settled in the pattan region of the Lahaul sub- division. As per Census of India, the population of Swangla Tribe stood at 9,630 (Males 4829 and females 4801).

Social status
, the Swangla were classified as a Scheduled Tribe under the Indian government's reservation program of positive discrimination.

References

Scheduled Tribes of Himachal Pradesh


Moving is a lot of work, whether you are shifting base of your home or you are doing it of your office, it can mean a tremendous amount of work. That is why you need to have a reliable removals service that can help you put everything you need together and then set it up in the new space.
Maids from a cleaning company are trained maid and they clean your kitchen, bathroom, bedroom, Living room and all the type of room they clean it and make the room shiny and sparkly. All the dust from your room and grimy residue are vanished from your house.
The success of your weekend DIY project depends entirely on the kind and the quality of tools you are using. Apart from your skills, the end product needs the right kind of DIY supplies for the right job.
Having blenders is good because they are able to draw out life from fruits and vegetables consequently creating a perfect blend full of goodies necessary for the normal body functioning. A blender is therefore quite an essential for the modern kitchen In particular. Even for crushing ice, blenders are still the leading choice. They have already proven to be far much better than immersion mixers and any other common food processors.
The dome you see today is not the first one that was made. This first one was the result of a Capitol design contest which came about on the request of President George Washington.
Green is in for the earth's benefit although black may have held the position for a long while. As more consumers are growing more conscious about the unhealthy condition of the earth, green measures are more obvious in many areas of the society; this includes cleaning services. Chichester city is no exception to this green movement where more commercial cleaners today are adopting green cleaning methods.
How To Keep Your Home Clean With Domestic Cleaning Bristol Service?


text:
//
//  PopoverViewCell.m
//  Popover
//
//  Created by StevenLee on 2016/12/10.
//  Copyright © 2016年 lifution. All rights reserved.
//

#import "PopoverViewCell.h"

// extern
float const PopoverViewCellHorizontalMargin = 15.f; ///< 水平边距
float const PopoverViewCellVerticalMargin = 3.f; ///< 垂直边距
float const PopoverViewCellTitleLeftEdge = 8.f; ///< 标题左边边距

@interface PopoverViewCell ()

@property (nonatomic, strong) UIButton *button;
@property (nonatomic, weak) UIView *bottomLine;

@end

@implementation PopoverViewCell

#pragma mark - Life Cycle
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (!(self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) return nil;
    self.backgroundColor = [UIColor clearColor];
    self.contentView.backgroundColor = self.backgroundColor;
    self.selectionStyle = UITableViewCellSelectionStyleNone;
    // initialize
    [self initialize];

    return self;
}

- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
    [super setHighlighted:highlighted animated:animated];
    if (highlighted) {
        self.backgroundColor = _style == PopoverViewStyleDefault ? [UIColor colorWithRed:0.90 green:0.90 blue:0.90 alpha:1.00] : [UIColor colorWithRed:0.23 green:0.23 blue:0.23 alpha:1.00];
    } else {
        [UIView animateWithDuration:0.3f animations:^{
            self.backgroundColor = [UIColor clearColor];
        }];
    }
}

#pragma mark - Setter
- (void)setStyle:(PopoverViewStyle)style {
    _style = style;
    _bottomLine.backgroundColor = [self.class bottomLineColorForStyle:style];
    if (_style == PopoverViewStyleDefault) {
        [_button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
    } else {
        [_button setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
    }
}

#pragma mark - Private
// 初始化
- (void)initialize {
    // UI
    _button = [UIButton buttonWithType:UIButtonTypeCustom];
    _button.userInteractionEnabled = NO; // has no use for UserInteraction.
    _button.translatesAutoresizingMaskIntoConstraints = NO;
    _button.titleLabel.font = [self.class titleFont];
    _button.backgroundColor = self.contentView.backgroundColor;
    _button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
    [_button setTitleColor:UIColor.blackColor forState:UIControlStateNormal];
    [self.contentView addSubview:_button];
    // Constraint
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_button]-margin-|" options:kNilOptions metrics:@{@"margin" : @(PopoverViewCellHorizontalMargin)} views:NSDictionaryOfVariableBindings(_button)]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-margin-[_button]-margin-|" options:kNilOptions metrics:@{@"margin" : @(PopoverViewCellVerticalMargin)} views:NSDictionaryOfVariableBindings(_button)]];
    // 底部线条
    UIView *bottomLine = [[UIView alloc] init];
    bottomLine.backgroundColor = [UIColor colorWithRed:0.75 green:0.75 blue:0.75 alpha:1.00];
    bottomLine.translatesAutoresizingMaskIntoConstraints = NO;
    [self.contentView addSubview:bottomLine];
    _bottomLine = bottomLine;
    // Constraint
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[bottomLine]|" options:kNilOptions metrics:nil views:NSDictionaryOfVariableBindings(bottomLine)]];
    [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[bottomLine(lineHeight)]|" options:kNilOptions metrics:@{@"lineHeight" : @(1/[UIScreen mainScreen].scale)} views:NSDictionaryOfVariableBindings(bottomLine)]];
}

#pragma mark - Public
/*! @brief 标题字体 */
+ (UIFont *)titleFont {
    return [UIFont systemFontOfSize:15];
}

/*! @brief 底部线条颜色 */
+ (UIColor *)bottomLineColorForStyle:(PopoverViewStyle)style {
    return style == PopoverViewStyleDefault ? [UIColor colorWithRed:0.75 green:0.75 blue:0.75 alpha:1.00] : [UIColor colorWithRed:0.4 green:0.4 blue:0.4 alpha:1.00];
}

- (void)setAction:(PopoverAction *)action {
    [_button setImage:action.image forState:UIControlStateNormal];
    [_button setTitle:action.title forState:UIControlStateNormal];
    _button.titleEdgeInsets = action.image ? UIEdgeInsetsMake(0, PopoverViewCellTitleLeftEdge, 0, -PopoverViewCellTitleLeftEdge) : UIEdgeInsetsZero;
}

- (void)showBottomLine:(BOOL)show {
    _bottomLine.hidden = !show;
}

@end


Gosfield Lawn Tennis Club is located in the village of Gosfield about 3 miles north of Braintree in Essex on the A1017, the club is signposted on the road into the village (Braintree Road). See Find Us for a map.
The club has four hard courts, two of which are floodlit.
We have coaching available for all ages and abilities, please see the Coaching page for more details.
The club enters mens, ladies and mixed teams in local leagues during the summer and has club nights every Friday evening during the summer months.
The club has over 60 members of all ages and standards and holds various competitions during the year - visit the Calendar page to see the details.
The club was formed in 1919 and moved to its present site in 1930, visit the history page to read the story of Gosfield Lawn Tennis club from its beginning to the present day. For more details go to the History page.
To find more information about how to join the club and details of the fees see the Membership page.