<commit_before>"""
Yahoo! Python SDK

 * Yahoo! Query Language
 * Yahoo! Social API

Find documentation and support on Yahoo! Developer Network: http://developer.yahoo.com

Hosted on GitHub: http://github.com/yahoo/yos-social-python/tree/master

@copyright: Copyrights for code authored by Yahoo! Inc. is licensed under the following terms:
@license:   BSD Open Source License

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
"""

__author__   = 'Dustin Whittle <dustin@yahoo-inc.com>'
__version__  = '0.1'

import time, random, oauthlib.oauth, simplejson

from . import oauth
from . import yql

# Yahoo! Social API
SOCIAL_API_URL = 'http://social.yahooapis.com/v1'

class OAuthApplicationException(Exception):
  pass

class OAuthApplication(object):

  def __init__(self, consumer_key, consumer_secret, application_id, callback_url = None, token = None, options = { 'lang': 'en' }):

    self.client = oauth.Client()

    self.consumer_key        = consumer_key
    self.consumer_secret     = consumer_secret
    self.application_id      = application_id
    self.callback_url        = callback_url
    self.token               = token
    self.options             = options

    self.consumer                   = oauthlib.oauth.OAuthConsumer(self.consumer_key, self.consumer_secret)
    self.signature_method_plaintext = oauthlib.oauth.OAuthSignatureMethod_PLAINTEXT()
    self.signature_method_hmac_sha1 = oauthlib.oauth.OAuthSignatureMethod_HMAC_SHA1()

  # oauth standard apis
  def get_request_token(self, callback = 'oob'):
    parameters = { 'xoauth_lang_pref': self.options['lang'], 'oauth_callback': callback }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, http_method='GET', http_url=self.client.request_token_url, parameters=parameters)
    request.sign_request(self.signature_method_plaintext, self.consumer, None)
    return self.client.fetch_request_token(request)

  def get_authorization_url(self, request_token):
    return oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=request_token, http_method='GET', http_url=self.client.authorization_url).to_url()

  def get_access_token(self, request_token, verifier=None):
    if verifier == None:
      parameters = { }
    else:
      parameters = { 'oauth_verifier': verifier }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=request_token, http_method='GET', http_url=self.client.access_token_url, parameters=parameters)
    request.sign_request(self.signature_method_plaintext, self.consumer, request_token)
    self.token = self.client.fetch_access_token(request)
    return self.token

  def refresh_access_token(self, access_token):
    parameters = { 'oauth_session_handle': access_token.session_handle }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=access_token, http_method='GET', http_url=self.client.access_token_url, parameters=parameters)
    request.sign_request(self.signature_method_plaintext, self.consumer, access_token)
    self.token = self.client.fetch_access_token(request)
    return self.token

  def getProfile(self, guid=None):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/profile' % guid
    parameters = { 'format': 'json' }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))['profile']
    except:
      return False

  def getPresence(self, guid=None):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/presence/presence' % guid
    parameters = { 'format': 'json' }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))['presence']
    except:
      return False

  def getConnections(self, guid=None, offset=0, limit=10000):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/connections' % guid
    parameters = { 'format': 'json', 'view': 'usercard', 'start': offset, 'count': limit }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def getContacts(self, offset=0, limit=10000):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json', 'view': 'tinyusercard', 'start': offset, 'count': limit }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def getContact(self, contact_id):
    url = SOCIAL_API_URL + '/user/%s/contact/%s' % (self.token.yahoo_guid, contact_id)
    parameters = { 'format': 'json' }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def addContact(self, contact):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json' }
    data = { 'contact': contact }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='POST', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request, simplejson.dumps(data)))
    except:
      return False

  def syncContacts(self, contact_sync):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json' }
    data = { 'contactsync': contact_sync }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='PUT', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request, simplejson.dumps(data)))
    except:
      return False

  def getContactSync(self, revision = 0):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json', 'view': 'sync', 'rev': revision }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def getUpdates(self, guid=None, offset=0, limit=10000):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/updates' % guid
    parameters = { 'format': 'json', 'start': offset, 'count': limit }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def insertUpdate(self, title, description, link, guid=None):
    if guid == None:
      guid = self.token.yahoo_guid

    source = "APP.%s" % self.application_id
    suid = 'ugc%s' % random.randrange(0, 101)
    parameters = { 'format': 'json' }
    body = '''
    { "updates":
        [
            {
                "class": "app",
                "collectionType": "guid",
                "description": "%s",
                "suid": "%s",
                "link": "%s",
                "source": "%s",
                "pubDate": "%s",
                "title": "%s",
                "type": "appActivity",
                "collectionID": "%s"
            }
        ]
    }''' % (description, suid, link, source, int(time.time()), title, guid)

    url = "%s/user/%s/updates/%s/%s" % (SOCIAL_API_URL, guid, source, suid)
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='PUT', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)

    try:
      return simplejson.loads(self.client.access_resource(request, body))
    except:
      return False

  def getSocialGraph(self, offset=0, limit=10000):
    return self.yql('select * from social.profile (%s, %s) where guid in (select guid from social.connections (%s, %s) where owner_guid=me)' % (offset, limit, offset, limit))['query']['results']['profile']

  def getGeoPlaces(self, location):
    return self.yql('select * from geo.places where text="' + location + '"')['query']['results']

  def yql(self, query):
    parameters = { 'q': query, 'format': 'json', 'env': yql.DATATABLES_URL }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=yql.OAUTH_API_URL, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False
<commit_msg>Change YahooAPI url to use https<commit_after>"""
Yahoo! Python SDK

 * Yahoo! Query Language
 * Yahoo! Social API

Find documentation and support on Yahoo! Developer Network: http://developer.yahoo.com

Hosted on GitHub: http://github.com/yahoo/yos-social-python/tree/master

@copyright: Copyrights for code authored by Yahoo! Inc. is licensed under the following terms:
@license:   BSD Open Source License

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
"""

__author__   = 'Dustin Whittle <dustin@yahoo-inc.com>'
__version__  = '0.1'

import time, random, oauthlib.oauth, simplejson

from . import oauth
from . import yql

# Yahoo! Social API
SOCIAL_API_URL = 'https://social.yahooapis.com/v1'

class OAuthApplicationException(Exception):
  pass

class OAuthApplication(object):

  def __init__(self, consumer_key, consumer_secret, application_id, callback_url = None, token = None, options = { 'lang': 'en' }):

    self.client = oauth.Client()

    self.consumer_key        = consumer_key
    self.consumer_secret     = consumer_secret
    self.application_id      = application_id
    self.callback_url        = callback_url
    self.token               = token
    self.options             = options

    self.consumer                   = oauthlib.oauth.OAuthConsumer(self.consumer_key, self.consumer_secret)
    self.signature_method_plaintext = oauthlib.oauth.OAuthSignatureMethod_PLAINTEXT()
    self.signature_method_hmac_sha1 = oauthlib.oauth.OAuthSignatureMethod_HMAC_SHA1()

  # oauth standard apis
  def get_request_token(self, callback = 'oob'):
    parameters = { 'xoauth_lang_pref': self.options['lang'], 'oauth_callback': callback }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, http_method='GET', http_url=self.client.request_token_url, parameters=parameters)
    request.sign_request(self.signature_method_plaintext, self.consumer, None)
    return self.client.fetch_request_token(request)

  def get_authorization_url(self, request_token):
    return oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=request_token, http_method='GET', http_url=self.client.authorization_url).to_url()

  def get_access_token(self, request_token, verifier=None):
    if verifier == None:
      parameters = { }
    else:
      parameters = { 'oauth_verifier': verifier }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=request_token, http_method='GET', http_url=self.client.access_token_url, parameters=parameters)
    request.sign_request(self.signature_method_plaintext, self.consumer, request_token)
    self.token = self.client.fetch_access_token(request)
    return self.token

  def refresh_access_token(self, access_token):
    parameters = { 'oauth_session_handle': access_token.session_handle }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=access_token, http_method='GET', http_url=self.client.access_token_url, parameters=parameters)
    request.sign_request(self.signature_method_plaintext, self.consumer, access_token)
    self.token = self.client.fetch_access_token(request)
    return self.token

  def getProfile(self, guid=None):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/profile' % guid
    parameters = { 'format': 'json' }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))['profile']
    except:
      return False

  def getPresence(self, guid=None):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/presence/presence' % guid
    parameters = { 'format': 'json' }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))['presence']
    except:
      return False

  def getConnections(self, guid=None, offset=0, limit=10000):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/connections' % guid
    parameters = { 'format': 'json', 'view': 'usercard', 'start': offset, 'count': limit }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def getContacts(self, offset=0, limit=10000):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json', 'view': 'tinyusercard', 'start': offset, 'count': limit }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def getContact(self, contact_id):
    url = SOCIAL_API_URL + '/user/%s/contact/%s' % (self.token.yahoo_guid, contact_id)
    parameters = { 'format': 'json' }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def addContact(self, contact):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json' }
    data = { 'contact': contact }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='POST', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request, simplejson.dumps(data)))
    except:
      return False

  def syncContacts(self, contact_sync):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json' }
    data = { 'contactsync': contact_sync }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='PUT', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request, simplejson.dumps(data)))
    except:
      return False

  def getContactSync(self, revision = 0):
    url = SOCIAL_API_URL + '/user/%s/contacts' % self.token.yahoo_guid
    parameters = { 'format': 'json', 'view': 'sync', 'rev': revision }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def getUpdates(self, guid=None, offset=0, limit=10000):
    if guid == None:
      guid = self.token.yahoo_guid
    url = SOCIAL_API_URL + '/user/%s/updates' % guid
    parameters = { 'format': 'json', 'start': offset, 'count': limit }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False

  def insertUpdate(self, title, description, link, guid=None):
    if guid == None:
      guid = self.token.yahoo_guid

    source = "APP.%s" % self.application_id
    suid = 'ugc%s' % random.randrange(0, 101)
    parameters = { 'format': 'json' }
    body = '''
    { "updates":
        [
            {
                "class": "app",
                "collectionType": "guid",
                "description": "%s",
                "suid": "%s",
                "link": "%s",
                "source": "%s",
                "pubDate": "%s",
                "title": "%s",
                "type": "appActivity",
                "collectionID": "%s"
            }
        ]
    }''' % (description, suid, link, source, int(time.time()), title, guid)

    url = "%s/user/%s/updates/%s/%s" % (SOCIAL_API_URL, guid, source, suid)
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='PUT', http_url=url, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)

    try:
      return simplejson.loads(self.client.access_resource(request, body))
    except:
      return False

  def getSocialGraph(self, offset=0, limit=10000):
    return self.yql('select * from social.profile (%s, %s) where guid in (select guid from social.connections (%s, %s) where owner_guid=me)' % (offset, limit, offset, limit))['query']['results']['profile']

  def getGeoPlaces(self, location):
    return self.yql('select * from geo.places where text="' + location + '"')['query']['results']

  def yql(self, query):
    parameters = { 'q': query, 'format': 'json', 'env': yql.DATATABLES_URL }
    request = oauthlib.oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.token, http_method='GET', http_url=yql.OAUTH_API_URL, parameters=parameters)
    request.sign_request(self.signature_method_hmac_sha1, self.consumer, self.token)
    try:
      return simplejson.loads(self.client.access_resource(request))
    except:
      return False
<|endoftext|>


<commit_before>'''
This software is released under an MIT/X11 open source license.

Copyright 2013-2014 Diffeo, Inc.
'''
import importlib
import logging
import pdb
import os

import yaml

logger = logging.getLogger('yakonfig')

__all__ = [
    'set_global_config',
    'get_global_config',
    'set_runtime_args_object',
    'set_runtime_args_dict',
]

_runtime_args_object = None
_runtime_args_dict = None

_config_file_path = None

_config_cache = None

def set_runtime_args_object(args):
    """Set an agrguments object, from argparse ArgumentParser.parse_args() or similar. Will be used for substituting into !runtime values in the config yaml."""
    global _runtime_args_object
    global _runtime_args_dict
    _runtime_args_object = args
    _runtime_args_dict = None

def set_runtime_args_dict(args):
    """Set a dictionary of global options. Will be used for substituting into !runtime values in the config yaml."""
    global _runtime_args_object
    global _runtime_args_dict
    _runtime_args_dict = args
    _runtime_args_object = None

class Loader(yaml.Loader):

    def __init__(self, stream):
        ## find root path for !include relative path
        streamname = getattr(stream, 'name', None)
        if streamname:
            self._root = os.path.dirname(streamname)
        else:
            self._root = None
        super(Loader, self).__init__(stream)

    def include_func(self, node):
        '''
        call a python function to inject values into the yaml, such as
        a function that provides default yaml for loading.
        '''
        mod_name = self.construct_scalar(node)
        parts = mod_name.split('.')
        if not len(parts) >= 2:
            raise Exception('!include_func expects full.path.to.func(), not %r' % mod_name)
        func_name = parts[-1]
        mod_name = '.'.join(parts[:-1])
        mod = importlib.import_module(mod_name)
        func = getattr(mod, func_name, None)
        if not func:
            raise Exception('%r not found in %r, dir(%r) = %r' % (func_name, mod_name, mod_name, dir(mod)))

        if func_name.endswith('yaml'):
            ## functions named ".*yaml$" must return YAML to which we
            ## apply this Loader
            return yaml.load(func(), Loader)
        else:
            return func()

    def include_yaml(self, node):
        '''
        load another yaml file from the path specified by node's value
        '''
        filename = self.construct_scalar(node)
        if not filename.startswith('/'):
            if self._root is None:
                raise Exception('!include_yaml %s is a relative path, but stream lacks path' % filename)
            filename = os.path.join(self._root, self.construct_scalar(node))
        with self.open(filename, 'r') as fin:
            return yaml.load(fin, Loader)

    def include_runtime(self, node):
        '''
        load another yaml file from the path specified by runtime arg
        named by node's value
        '''
        runtimedict = _runtime_args_dict or vars(_runtime_args_object)
        filename = runtimedict.get(node.value)
        if filename is None:
            raise Exception('%r not in runtime args: %r' % (node.value, runtimedict))
        if not filename.startswith('/'):
            if self._root is None:
                raise Exception('!include_runtime %s is a relative path, but stream lacks path' % filename)
            filename = os.path.join(self._root, self.construct_scalar(node))
        with self.open(filename, 'r') as fin:
            return yaml.load(fin, Loader)

    def open(self, *args, **kwargs):
        '''
        method that looks like the regular python builtin `open`, and
        an be replaced by tests with different behavior
        '''
        return open(*args, **kwargs)

    def runtime(self, node):
        '''
        provide !runtime values from having set_runtime_args_dict or
        set_runtime_args_object
        '''
        runtimedict = _runtime_args_dict or vars(_runtime_args_object)
        if (node is None) or (not node.value):
            return runtimedict  # with no specifier, return the whole thing
        return runtimedict.get(node.value)

Loader.add_constructor('!include_runtime', Loader.include_runtime)
Loader.add_constructor('!include_func', Loader.include_func)
Loader.add_constructor('!include_yaml', Loader.include_yaml)
Loader.add_constructor('!runtime', Loader.runtime)

def set_global_config(path=None, stream=None):
    """Usage: call this from main() with a path or stream object.
    Calling it repeatedly with the same path is safe.
    """
    assert path or stream
    global _config_file_path
    global _config_cache
    if path is None:
        path = _config_file_path
        # TODO: os.environ[{$0}_CONFIG] ?
    else:
        if _config_file_path is None:
            _config_file_path = path
        elif _config_file_path == path:
            pass  # okay!
        else:
            raise Exception("disparate paths attempted to be used for global config path: %r %r" % (_config_file_path, path))

    if _config_cache is not None:
        assert not stream
        return _config_cache

    if path:
        assert not stream
        fin = open(path)
    else:
        assert stream
        fin = stream

    _config_cache = yaml.load(fin, Loader)
    # TODO: convert to frozen dict?
    return _config_cache

def get_global_config(top_level_name=None):
    global _config_cache
    if top_level_name is not None:
        if _config_cache is None:
            raise KeyError('_config_cache has not been set, so %s is not in it' % top_level_name)
        elif top_level_name not in _config_cache:
            raise KeyError('%r not in %r' % (top_level_name, _config_cache.keys()))
        else:
            return _config_cache.get(top_level_name)
    else:
        return _config_cache
<commit_msg>replacing assert with Exception<commit_after>'''
This software is released under an MIT/X11 open source license.

Copyright 2013-2014 Diffeo, Inc.
'''
import importlib
import logging
import pdb
import os

import yaml

logger = logging.getLogger('yakonfig')

__all__ = [
    'set_global_config',
    'get_global_config',
    'set_runtime_args_object',
    'set_runtime_args_dict',
    'Loader',
]

_runtime_args_object = None
_runtime_args_dict = None

_config_file_path = None

_config_cache = None

def set_runtime_args_object(args):
    """Set an agrguments object, from argparse ArgumentParser.parse_args() or similar. Will be used for substituting into !runtime values in the config yaml."""
    global _runtime_args_object
    global _runtime_args_dict
    _runtime_args_object = args
    _runtime_args_dict = None

def set_runtime_args_dict(args):
    """Set a dictionary of global options. Will be used for substituting into !runtime values in the config yaml."""
    global _runtime_args_object
    global _runtime_args_dict
    _runtime_args_dict = args
    _runtime_args_object = None

class Loader(yaml.Loader):

    def __init__(self, stream):
        ## find root path for !include relative path
        streamname = getattr(stream, 'name', None)
        if streamname:
            self._root = os.path.dirname(streamname)
        else:
            self._root = None
        super(Loader, self).__init__(stream)

    def include_func(self, node):
        '''
        call a python function to inject values into the yaml, such as
        a function that provides default yaml for loading.
        '''
        mod_name = self.construct_scalar(node)
        parts = mod_name.split('.')
        if not len(parts) >= 2:
            raise Exception('!include_func expects full.path.to.func(), not %r' % mod_name)
        func_name = parts[-1]
        mod_name = '.'.join(parts[:-1])
        mod = importlib.import_module(mod_name)
        func = getattr(mod, func_name, None)
        if not func:
            raise Exception('%r not found in %r, dir(%r) = %r' % (func_name, mod_name, mod_name, dir(mod)))

        if func_name.endswith('yaml'):
            ## functions named ".*yaml$" must return YAML to which we
            ## apply this Loader
            return yaml.load(func(), Loader)
        else:
            return func()

    def include_yaml(self, node):
        '''
        load another yaml file from the path specified by node's value
        '''
        filename = self.construct_scalar(node)
        if not filename.startswith('/'):
            if self._root is None:
                raise Exception('!include_yaml %s is a relative path, but stream lacks path' % filename)
            filename = os.path.join(self._root, self.construct_scalar(node))
        with self.open(filename, 'r') as fin:
            return yaml.load(fin, Loader)

    def include_runtime(self, node):
        '''
        load another yaml file from the path specified by runtime arg
        named by node's value
        '''
        runtimedict = _runtime_args_dict or vars(_runtime_args_object)
        filename = runtimedict.get(node.value)
        if filename is None:
            raise Exception('%r not in runtime args: %r' % (node.value, runtimedict))
        if not filename.startswith('/'):
            if self._root is None:
                raise Exception('!include_runtime %s is a relative path, but stream lacks path' % filename)
            filename = os.path.join(self._root, self.construct_scalar(node))
        with self.open(filename, 'r') as fin:
            return yaml.load(fin, Loader)

    def open(self, *args, **kwargs):
        '''
        method that looks like the regular python builtin `open`, and
        an be replaced by tests with different behavior
        '''
        return open(*args, **kwargs)

    def runtime(self, node):
        '''
        provide !runtime values from having set_runtime_args_dict or
        set_runtime_args_object
        '''
        runtimedict = _runtime_args_dict or vars(_runtime_args_object)
        if (node is None) or (not node.value):
            return runtimedict  # with no specifier, return the whole thing
        return runtimedict.get(node.value)

Loader.add_constructor('!include_runtime', Loader.include_runtime)
Loader.add_constructor('!include_func', Loader.include_func)
Loader.add_constructor('!include_yaml', Loader.include_yaml)
Loader.add_constructor('!runtime', Loader.runtime)

def set_global_config(path=None, stream=None):
    """Usage: call this from main() with a path or stream object.
    Calling it repeatedly with the same path is safe.
    """
    if path is None and stream is None:
        raise Exception('set_global_config requires either "path" or "stream"')
    global _config_file_path
    global _config_cache
    if path is None:
        path = _config_file_path
        # TODO: os.environ[{$0}_CONFIG] ?
    else:
        if _config_file_path is None:
            _config_file_path = path
        elif _config_file_path == path:
            pass  # okay!
        else:
            raise Exception("disparate paths attempted to be used for global config path: %r %r" % (_config_file_path, path))

    if _config_cache is not None:
        assert not stream
        return _config_cache

    if path:
        assert not stream
        fin = open(path)
    else:
        assert stream
        fin = stream

    _config_cache = yaml.load(fin, Loader)
    # TODO: convert to frozen dict?
    return _config_cache

def get_global_config(top_level_name=None):
    global _config_cache
    if top_level_name is not None:
        if _config_cache is None:
            raise KeyError('_config_cache has not been set, so %s is not in it' % top_level_name)
        elif top_level_name not in _config_cache:
            raise KeyError('%r not in %r' % (top_level_name, _config_cache.keys()))
        else:
            return _config_cache.get(top_level_name)
    else:
        return _config_cache
<|endoftext|>


<commit_before>import os
import json
import subprocess

from datetime import datetime
from collections import OrderedDict

from moulinette import m18n
from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.log import getActionLogger
from yunohost.regenconf import regen_conf

logger = getActionLogger("yunohost.settings")

SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"

def is_boolean(value):
    """
    Ensure a string value is intended as a boolean

    Keyword arguments:
        arg -- The string to check

    Returns:
        (is_boolean, boolean_value)

    """
    if isinstance(value, bool):
        return True, value
    elif isinstance(value, str):
        if str(value).lower() in ["true", "on", "yes", "false", "off", "no"]:
            return True, str(value).lower() in ["true", "on", "yes"]
        else:
            return False, None
    else:
        return False, None

# a settings entry is in the form of:
# namespace.subnamespace.name: {type, value, default, description, [choices]}
# choices is only for enum
# the keyname can have as many subnamespace as needed but should have at least
# one level of namespace

# description is implied from the translated strings
# the key is "global_settings_setting_%s" % key.replace(".", "_")

# type can be:
# * bool
# * int
# * string
# * enum (in the form of a python list)

DEFAULTS = OrderedDict(
    [
        # Password Validation
        # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
        ("security.password.admin.strength", {"type": "int", "default": 1}),
        ("security.password.user.strength", {"type": "int", "default": 1}),
        (
            "service.ssh.allow_deprecated_dsa_hostkey",
            {"type": "bool", "default": False},
        ),
        (
            "security.ssh.compatibility",
            {
                "type": "enum",
                "default": "modern",
                "choices": ["intermediate", "modern"],
            },
        ),
        (
            "security.ssh.port",
            {"type": "int", "default": 22},
        ),
        (
            "security.nginx.compatibility",
            {
                "type": "enum",
                "default": "intermediate",
                "choices": ["intermediate", "modern"],
            },
        ),
        (
            "security.postfix.compatibility",
            {
                "type": "enum",
                "default": "intermediate",
                "choices": ["intermediate", "modern"],
            },
        ),
        ("pop3.enabled", {"type": "bool", "default": False}),
        ("smtp.allow_ipv6", {"type": "bool", "default": True}),
        ("smtp.relay.host", {"type": "string", "default": ""}),
        ("smtp.relay.port", {"type": "int", "default": 587}),
        ("smtp.relay.user", {"type": "string", "default": ""}),
        ("smtp.relay.password", {"type": "string", "default": ""}),
        ("backup.compress_tar_archives", {"type": "bool", "default": False}),
        ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}),
    ]
)

def settings_get(key, full=False):
    """
    Get an entry value in the settings

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key)

    if full:
        return settings[key]

    return settings[key]["value"]

def settings_list():
    """
    List all entries of the settings

    """
    return _get_settings()

def settings_set(key, value):
    """
    Set an entry value in the settings

    Keyword argument:
        key -- Settings key
        value -- New value

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key)

    key_type = settings[key]["type"]

    if key_type == "bool":
        boolean_value = is_boolean(value)
        if boolean_value[0]:
            value = boolean_value[1]
        else:
            raise YunohostValidationError(
                "global_settings_bad_type_for_setting",
                setting=key,
                received_type=type(value).__name__,
                expected_type=key_type,
            )
    elif key_type == "int":
        if not isinstance(value, int) or isinstance(value, bool):
            if isinstance(value, str):
                try:
                    value = int(value)
                except Exception:
                    raise YunohostValidationError(
                        "global_settings_bad_type_for_setting",
                        setting=key,
                        received_type=type(value).__name__,
                        expected_type=key_type,
                    )
            else:
                raise YunohostValidationError(
                    "global_settings_bad_type_for_setting",
                    setting=key,
                    received_type=type(value).__name__,
                    expected_type=key_type,
                )
    elif key_type == "string":
        if not isinstance(value, str):
            raise YunohostValidationError(
                "global_settings_bad_type_for_setting",
                setting=key,
                received_type=type(value).__name__,
                expected_type=key_type,
            )
    elif key_type == "enum":
        if value not in settings[key]["choices"]:
            raise YunohostValidationError(
                "global_settings_bad_choice_for_enum",
                setting=key,
                choice=str(value),
                available_choices=", ".join(settings[key]["choices"]),
            )
    else:
        raise YunohostValidationError(
            "global_settings_unknown_type", setting=key, unknown_type=key_type
        )

    old_value = settings[key].get("value")
    settings[key]["value"] = value
    _save_settings(settings)

    try:
        trigger_post_change_hook(key, old_value, value)
    except Exception as e:
        logger.error("Post-change hook for setting %s failed : %s" % (key, e))
        raise

def settings_reset(key):
    """
    Set an entry value to its default one

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key)

    settings[key]["value"] = settings[key]["default"]
    _save_settings(settings)

def settings_reset_all():
    """
    Reset all settings to their default value

    Keyword argument:
        yes -- Yes I'm sure I want to do that

    """
    settings = _get_settings()

    # For now on, we backup the previous settings in case of but we don't have
    # any mecanism to take advantage of those backups. It could be a nice
    # addition but we'll see if this is a common need.
    # Another solution would be to use etckeeper and integrate those
    # modification inside of it and take advantage of its git history
    old_settings_backup_path = (
        SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
    )
    _save_settings(settings, location=old_settings_backup_path)

    for value in settings.values():
        value["value"] = value["default"]

    _save_settings(settings)

    return {
        "old_settings_backup_path": old_settings_backup_path,
        "message": m18n.n(
            "global_settings_reset_success", path=old_settings_backup_path
        ),
    }

def _get_settings():
    def get_setting_description(key):
        if key.startswith("example"):
            # (This is for dummy stuff used during unit tests)
            return "Dummy %s setting" % key.split(".")[-1]
        return m18n.n("global_settings_setting_%s" % key.replace(".", "_"))

    settings = {}

    for key, value in DEFAULTS.copy().items():
        settings[key] = value
        settings[key]["value"] = value["default"]
        settings[key]["description"] = get_setting_description(key)

    if not os.path.exists(SETTINGS_PATH):
        return settings

    # we have a very strict policy on only allowing settings that we know in
    # the OrderedDict DEFAULTS
    # For various reason, while reading the local settings we might encounter
    # settings that aren't in DEFAULTS, those can come from settings key that
    # we have removed, errors or the user trying to modify
    # /etc/yunohost/settings.json
    # To avoid to simply overwrite them, we store them in
    # /etc/yunohost/settings-unknown.json in case of
    unknown_settings = {}
    unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown"

    if os.path.exists(unknown_settings_path):
        try:
            unknown_settings = json.load(open(unknown_settings_path, "r"))
        except Exception as e:
            logger.warning("Error while loading unknown settings %s" % e)

    try:
        with open(SETTINGS_PATH) as settings_fd:
            local_settings = json.load(settings_fd)

            for key, value in local_settings.items():
                if key in settings:
                    settings[key] = value
                    settings[key]["description"] = get_setting_description(key)
                else:
                    logger.warning(
                        m18n.n(
                            "global_settings_unknown_setting_from_settings_file",
                            setting_key=key,
                        )
                    )
                    unknown_settings[key] = value
    except Exception as e:
        raise YunohostValidationError("global_settings_cant_open_settings", reason=e)

    if unknown_settings:
        try:
            _save_settings(unknown_settings, location=unknown_settings_path)
            _save_settings(settings)
        except Exception as e:
            logger.warning(
                "Failed to save unknown settings (because %s), aborting." % e
            )

    return settings

def _save_settings(settings, location=SETTINGS_PATH):
    settings_without_description = {}
    for key, value in settings.items():
        settings_without_description[key] = value
        if "description" in value:
            del settings_without_description[key]["description"]

    try:
        result = json.dumps(settings_without_description, indent=4)
    except Exception as e:
        raise YunohostError("global_settings_cant_serialize_settings", reason=e)

    try:
        with open(location, "w") as settings_fd:
            settings_fd.write(result)
    except Exception as e:
        raise YunohostError("global_settings_cant_write_settings", reason=e)

# Meant to be a dict of setting_name -> function to call
post_change_hooks = {}

def post_change_hook(setting_name):
    def decorator(func):
        assert setting_name in DEFAULTS.keys(), (
            "The setting %s does not exists" % setting_name
        )
        assert setting_name not in post_change_hooks, (
            "You can only register one post change hook per setting (in particular for %s)"
            % setting_name
        )
        post_change_hooks[setting_name] = func
        return func

    return decorator

def trigger_post_change_hook(setting_name, old_value, new_value):
    if setting_name not in post_change_hooks:
        logger.debug("Nothing to do after changing setting %s" % setting_name)
        return

    f = post_change_hooks[setting_name]
    f(setting_name, old_value, new_value)

# ===========================================
#
# Actions to trigger when changing a setting
# You can define such an action with :
#
# @post_change_hook("your.setting.name")
# def some_function_name(setting_name, old_value, new_value):
#     # Do some stuff
#
# ===========================================

@post_change_hook("ssowat.panel_overlay.enabled")
@post_change_hook("security.nginx.compatibility")
def reconfigure_nginx(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["nginx"])

@post_change_hook("security.ssh.compatibility")
def reconfigure_ssh(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["ssh"])

@post_change_hook("security.ssh.port")
def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["ssh", "fail2ban"])

@post_change_hook("smtp.allow_ipv6")
@post_change_hook("smtp.relay.host")
@post_change_hook("smtp.relay.port")
@post_change_hook("smtp.relay.user")
@post_change_hook("smtp.relay.password")
@post_change_hook("security.postfix.compatibility")
def reconfigure_postfix(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["postfix"])

@post_change_hook("pop3.enabled")
def reconfigure_dovecot(setting_name, old_value, new_value):
    dovecot_package = "dovecot-pop3d"

    environment = os.environ.copy()
    environment.update({"DEBIAN_FRONTEND": "noninteractive"})

    if new_value == "True":
        command = [
            "apt-get",
            "-y",
            "--no-remove",
            "-o Dpkg::Options::=--force-confdef",
            "-o Dpkg::Options::=--force-confold",
            "install",
            dovecot_package,
        ]
        subprocess.call(command, env=environment)
        if old_value != new_value:
            regen_conf(names=["dovecot"])
    else:
        if old_value != new_value:
            regen_conf(names=["dovecot"])
        command = ["apt-get", "-y", "remove", dovecot_package]
        subprocess.call(command, env=environment)
<commit_msg>Also reload firewall when changing ssh port<commit_after>import os
import json
import subprocess

from datetime import datetime
from collections import OrderedDict

from moulinette import m18n
from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.log import getActionLogger
from yunohost.regenconf import regen_conf
from yunohost.firewall import firewall_reload

logger = getActionLogger("yunohost.settings")

SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"

def is_boolean(value):
    """
    Ensure a string value is intended as a boolean

    Keyword arguments:
        arg -- The string to check

    Returns:
        (is_boolean, boolean_value)

    """
    if isinstance(value, bool):
        return True, value
    elif isinstance(value, str):
        if str(value).lower() in ["true", "on", "yes", "false", "off", "no"]:
            return True, str(value).lower() in ["true", "on", "yes"]
        else:
            return False, None
    else:
        return False, None

# a settings entry is in the form of:
# namespace.subnamespace.name: {type, value, default, description, [choices]}
# choices is only for enum
# the keyname can have as many subnamespace as needed but should have at least
# one level of namespace

# description is implied from the translated strings
# the key is "global_settings_setting_%s" % key.replace(".", "_")

# type can be:
# * bool
# * int
# * string
# * enum (in the form of a python list)

DEFAULTS = OrderedDict(
    [
        # Password Validation
        # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
        ("security.password.admin.strength", {"type": "int", "default": 1}),
        ("security.password.user.strength", {"type": "int", "default": 1}),
        (
            "service.ssh.allow_deprecated_dsa_hostkey",
            {"type": "bool", "default": False},
        ),
        (
            "security.ssh.compatibility",
            {
                "type": "enum",
                "default": "modern",
                "choices": ["intermediate", "modern"],
            },
        ),
        (
            "security.ssh.port",
            {"type": "int", "default": 22},
        ),
        (
            "security.nginx.compatibility",
            {
                "type": "enum",
                "default": "intermediate",
                "choices": ["intermediate", "modern"],
            },
        ),
        (
            "security.postfix.compatibility",
            {
                "type": "enum",
                "default": "intermediate",
                "choices": ["intermediate", "modern"],
            },
        ),
        ("pop3.enabled", {"type": "bool", "default": False}),
        ("smtp.allow_ipv6", {"type": "bool", "default": True}),
        ("smtp.relay.host", {"type": "string", "default": ""}),
        ("smtp.relay.port", {"type": "int", "default": 587}),
        ("smtp.relay.user", {"type": "string", "default": ""}),
        ("smtp.relay.password", {"type": "string", "default": ""}),
        ("backup.compress_tar_archives", {"type": "bool", "default": False}),
        ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}),
    ]
)

def settings_get(key, full=False):
    """
    Get an entry value in the settings

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key)

    if full:
        return settings[key]

    return settings[key]["value"]

def settings_list():
    """
    List all entries of the settings

    """
    return _get_settings()

def settings_set(key, value):
    """
    Set an entry value in the settings

    Keyword argument:
        key -- Settings key
        value -- New value

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key)

    key_type = settings[key]["type"]

    if key_type == "bool":
        boolean_value = is_boolean(value)
        if boolean_value[0]:
            value = boolean_value[1]
        else:
            raise YunohostValidationError(
                "global_settings_bad_type_for_setting",
                setting=key,
                received_type=type(value).__name__,
                expected_type=key_type,
            )
    elif key_type == "int":
        if not isinstance(value, int) or isinstance(value, bool):
            if isinstance(value, str):
                try:
                    value = int(value)
                except Exception:
                    raise YunohostValidationError(
                        "global_settings_bad_type_for_setting",
                        setting=key,
                        received_type=type(value).__name__,
                        expected_type=key_type,
                    )
            else:
                raise YunohostValidationError(
                    "global_settings_bad_type_for_setting",
                    setting=key,
                    received_type=type(value).__name__,
                    expected_type=key_type,
                )
    elif key_type == "string":
        if not isinstance(value, str):
            raise YunohostValidationError(
                "global_settings_bad_type_for_setting",
                setting=key,
                received_type=type(value).__name__,
                expected_type=key_type,
            )
    elif key_type == "enum":
        if value not in settings[key]["choices"]:
            raise YunohostValidationError(
                "global_settings_bad_choice_for_enum",
                setting=key,
                choice=str(value),
                available_choices=", ".join(settings[key]["choices"]),
            )
    else:
        raise YunohostValidationError(
            "global_settings_unknown_type", setting=key, unknown_type=key_type
        )

    old_value = settings[key].get("value")
    settings[key]["value"] = value
    _save_settings(settings)

    try:
        trigger_post_change_hook(key, old_value, value)
    except Exception as e:
        logger.error("Post-change hook for setting %s failed : %s" % (key, e))
        raise

def settings_reset(key):
    """
    Set an entry value to its default one

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key)

    settings[key]["value"] = settings[key]["default"]
    _save_settings(settings)

def settings_reset_all():
    """
    Reset all settings to their default value

    Keyword argument:
        yes -- Yes I'm sure I want to do that

    """
    settings = _get_settings()

    # For now on, we backup the previous settings in case of but we don't have
    # any mecanism to take advantage of those backups. It could be a nice
    # addition but we'll see if this is a common need.
    # Another solution would be to use etckeeper and integrate those
    # modification inside of it and take advantage of its git history
    old_settings_backup_path = (
        SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
    )
    _save_settings(settings, location=old_settings_backup_path)

    for value in settings.values():
        value["value"] = value["default"]

    _save_settings(settings)

    return {
        "old_settings_backup_path": old_settings_backup_path,
        "message": m18n.n(
            "global_settings_reset_success", path=old_settings_backup_path
        ),
    }

def _get_settings():
    def get_setting_description(key):
        if key.startswith("example"):
            # (This is for dummy stuff used during unit tests)
            return "Dummy %s setting" % key.split(".")[-1]
        return m18n.n("global_settings_setting_%s" % key.replace(".", "_"))

    settings = {}

    for key, value in DEFAULTS.copy().items():
        settings[key] = value
        settings[key]["value"] = value["default"]
        settings[key]["description"] = get_setting_description(key)

    if not os.path.exists(SETTINGS_PATH):
        return settings

    # we have a very strict policy on only allowing settings that we know in
    # the OrderedDict DEFAULTS
    # For various reason, while reading the local settings we might encounter
    # settings that aren't in DEFAULTS, those can come from settings key that
    # we have removed, errors or the user trying to modify
    # /etc/yunohost/settings.json
    # To avoid to simply overwrite them, we store them in
    # /etc/yunohost/settings-unknown.json in case of
    unknown_settings = {}
    unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown"

    if os.path.exists(unknown_settings_path):
        try:
            unknown_settings = json.load(open(unknown_settings_path, "r"))
        except Exception as e:
            logger.warning("Error while loading unknown settings %s" % e)

    try:
        with open(SETTINGS_PATH) as settings_fd:
            local_settings = json.load(settings_fd)

            for key, value in local_settings.items():
                if key in settings:
                    settings[key] = value
                    settings[key]["description"] = get_setting_description(key)
                else:
                    logger.warning(
                        m18n.n(
                            "global_settings_unknown_setting_from_settings_file",
                            setting_key=key,
                        )
                    )
                    unknown_settings[key] = value
    except Exception as e:
        raise YunohostValidationError("global_settings_cant_open_settings", reason=e)

    if unknown_settings:
        try:
            _save_settings(unknown_settings, location=unknown_settings_path)
            _save_settings(settings)
        except Exception as e:
            logger.warning(
                "Failed to save unknown settings (because %s), aborting." % e
            )

    return settings

def _save_settings(settings, location=SETTINGS_PATH):
    settings_without_description = {}
    for key, value in settings.items():
        settings_without_description[key] = value
        if "description" in value:
            del settings_without_description[key]["description"]

    try:
        result = json.dumps(settings_without_description, indent=4)
    except Exception as e:
        raise YunohostError("global_settings_cant_serialize_settings", reason=e)

    try:
        with open(location, "w") as settings_fd:
            settings_fd.write(result)
    except Exception as e:
        raise YunohostError("global_settings_cant_write_settings", reason=e)

# Meant to be a dict of setting_name -> function to call
post_change_hooks = {}

def post_change_hook(setting_name):
    def decorator(func):
        assert setting_name in DEFAULTS.keys(), (
            "The setting %s does not exists" % setting_name
        )
        assert setting_name not in post_change_hooks, (
            "You can only register one post change hook per setting (in particular for %s)"
            % setting_name
        )
        post_change_hooks[setting_name] = func
        return func

    return decorator

def trigger_post_change_hook(setting_name, old_value, new_value):
    if setting_name not in post_change_hooks:
        logger.debug("Nothing to do after changing setting %s" % setting_name)
        return

    f = post_change_hooks[setting_name]
    f(setting_name, old_value, new_value)

# ===========================================
#
# Actions to trigger when changing a setting
# You can define such an action with :
#
# @post_change_hook("your.setting.name")
# def some_function_name(setting_name, old_value, new_value):
#     # Do some stuff
#
# ===========================================

@post_change_hook("ssowat.panel_overlay.enabled")
@post_change_hook("security.nginx.compatibility")
def reconfigure_nginx(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["nginx"])

@post_change_hook("security.ssh.compatibility")
def reconfigure_ssh(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["ssh"])

@post_change_hook("security.ssh.port")
def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["ssh", "fail2ban"])
        firewall_reload()

@post_change_hook("smtp.allow_ipv6")
@post_change_hook("smtp.relay.host")
@post_change_hook("smtp.relay.port")
@post_change_hook("smtp.relay.user")
@post_change_hook("smtp.relay.password")
@post_change_hook("security.postfix.compatibility")
def reconfigure_postfix(setting_name, old_value, new_value):
    if old_value != new_value:
        regen_conf(names=["postfix"])

@post_change_hook("pop3.enabled")
def reconfigure_dovecot(setting_name, old_value, new_value):
    dovecot_package = "dovecot-pop3d"

    environment = os.environ.copy()
    environment.update({"DEBIAN_FRONTEND": "noninteractive"})

    if new_value == "True":
        command = [
            "apt-get",
            "-y",
            "--no-remove",
            "-o Dpkg::Options::=--force-confdef",
            "-o Dpkg::Options::=--force-confold",
            "install",
            dovecot_package,
        ]
        subprocess.call(command, env=environment)
        if old_value != new_value:
            regen_conf(names=["dovecot"])
    else:
        if old_value != new_value:
            regen_conf(names=["dovecot"])
        command = ["apt-get", "-y", "remove", dovecot_package]
        subprocess.call(command, env=environment)
<|endoftext|>


<commit_before>import os
import subprocess as ssubprocess
from sshuttle.methods import BaseMethod
from sshuttle.helpers import log, debug1, debug2, debug3, \
    Fatal, family_to_string, get_env, which

import socket

IP_BINDANY = 24
IP_RECVDSTADDR = 7
SOL_IPV6 = 41
IPV6_RECVDSTADDR = 74

def recv_udp(listener, bufsize):
    debug3('Accept UDP python using recvmsg.')
    data, ancdata, _, srcip = listener.recvmsg(4096,
                                               socket.CMSG_SPACE(4))
    dstip = None
    for cmsg_level, cmsg_type, cmsg_data in ancdata:
        if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
            port = 53
            ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
            dstip = (ip, port)
            break
    return (srcip, dstip, data)

def ipfw_rule_exists(n):
    argv = ['ipfw', 'list', '%d' % n]
    p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())

    found = False
    for line in p.stdout:
        if line.startswith(b'%05d ' % n):
            if not 'check-state :sshuttle' in line:
                log('non-sshuttle ipfw rule: %r' % line.strip())
                raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
            found = True
            break
    rv = p.wait()
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
    return found

_oldctls = {}

def _fill_oldctls(prefix):
    argv = ['sysctl', prefix]
    p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
    for line in p.stdout:
        line = line.decode()
        assert(line[-1] == '\n')
        (k, v) = line[:-1].split(': ', 1)
        _oldctls[k] = v.strip()
    rv = p.wait()
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
    if not line:
        raise Fatal('%r returned no data' % (argv,))

def _sysctl_set(name, val):
    argv = ['sysctl', '-w', '%s=%s' % (name, val)]
    debug1('>> %s' % ' '.join(argv))
    return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
    # No env: No output. (Or error that won't be parsed.)

_changedctls = []

def sysctl_set(name, val, permanent=False):
    PREFIX = 'net.inet.ip'
    assert(name.startswith(PREFIX + '.'))
    val = str(val)
    if not _oldctls:
        _fill_oldctls(PREFIX)
    if not (name in _oldctls):
        debug1('>> No such sysctl: %r' % name)
        return False
    oldval = _oldctls[name]
    if val != oldval:
        rv = _sysctl_set(name, val)
        if rv == 0 and permanent:
            debug1('>>   ...saving permanently in /etc/sysctl.conf')
            f = open('/etc/sysctl.conf', 'a')
            f.write('\n'
                    '# Added by sshuttle\n'
                    '%s=%s\n' % (name, val))
            f.close()
        else:
            _changedctls.append(name)
        return True

def ipfw(*args):
    argv = ['ipfw', '-q'] + list(args)
    debug1('>> %s' % ' '.join(argv))
    rv = ssubprocess.call(argv, env=get_env())
    # No env: No output. (Or error that won't be parsed.)
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))

def ipfw_noexit(*args):
    argv = ['ipfw', '-q'] + list(args)
    debug1('>> %s' % ' '.join(argv))
    ssubprocess.call(argv, env=get_env())
    # No env: No output. (Or error that won't be parsed.)

class Method(BaseMethod):

    def get_supported_features(self):
        result = super(Method, self).get_supported_features()
        result.ipv6 = False
        result.udp = False  # NOTE: Almost there, kernel patch needed
        result.dns = True
        return result

    def get_tcp_dstip(self, sock):
        return sock.getsockname()

    def recv_udp(self, udp_listener, bufsize):
        srcip, dstip, data = recv_udp(udp_listener, bufsize)
        if not dstip:
            debug1(
                   "-- ignored UDP from %r: "
                   "couldn't determine destination IP address" % (srcip,))
            return None
        return srcip, dstip, data

    def send_udp(self, sock, srcip, dstip, data):
        if not srcip:
            debug1(
               "-- ignored UDP to %r: "
               "couldn't determine source IP address" % (dstip,))
            return

        # debug3('Sending SRC: %r DST: %r' % (srcip, dstip))
        sender = socket.socket(sock.family, socket.SOCK_DGRAM)
        sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
        sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        sender.bind(srcip)
        sender.sendto(data, dstip)
        sender.close()

    def setup_udp_listener(self, udp_listener):
        if udp_listener.v4 is not None:
            udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
        # if udp_listener.v6 is not None:
        #     udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)

    def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
                       user, tmark):
        # IPv6 not supported
        if family not in [socket.AF_INET]:
            raise Exception(
                'Address family "%s" unsupported by ipfw method_name'
                % family_to_string(family))

        # XXX: Any risk from this?
        ipfw_noexit('delete', '1')

        while _changedctls:
            name = _changedctls.pop()
            oldval = _oldctls[name]
            _sysctl_set(name, oldval)

        if subnets or dnsport:
            sysctl_set('net.inet.ip.fw.enable', 1)

        ipfw('add', '1', 'check-state', ':sshuttle')

        ipfw('add', '1', 'skipto', '2',
             'tcp',
             'from', 'any', 'to', 'table(125)')
        ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
             'tcp',
             'from', 'any', 'to', 'table(126)',
             'setup', 'keep-state', ':sshuttle')

        ipfw_noexit('table', '124', 'flush')
        dnscount = 0
        for _, ip in [i for i in nslist if i[0] == family]:
            ipfw('table', '124', 'add', '%s' % (ip))
            dnscount += 1
        if dnscount > 0:
            ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
                 'udp',
                 'from', 'any', 'to', 'table(124)',
                 'keep-state', ':sshuttle')
        ipfw('add', '1', 'allow',
             'udp',
             'from', 'any', 'to', 'any')

        if subnets:
            # create new subnet entries
            for _, swidth, sexclude, snet, fport, lport in sorted(subnets,
                                                    key=lambda s: s[1],
                                                    reverse=True):
                if sexclude:
                    ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
                else:
                    ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))

    def restore_firewall(self, port, family, udp, user):
        if family not in [socket.AF_INET]:
            raise Exception(
                'Address family "%s" unsupported by ipfw method'
                % family_to_string(family))

        ipfw_noexit('delete', '1')
        ipfw_noexit('table', '124', 'flush')
        ipfw_noexit('table', '125', 'flush')
        ipfw_noexit('table', '126', 'flush')

    def is_supported(self):
        if which("ipfw"):
            return True
        debug2("ipfw method not supported because 'ipfw' command is "
               "missing.")
        return False
<commit_msg>Make flake8 happy.<commit_after>import os
import subprocess as ssubprocess
from sshuttle.methods import BaseMethod
from sshuttle.helpers import log, debug1, debug2, debug3, \
    Fatal, family_to_string, get_env, which

import socket

IP_BINDANY = 24
IP_RECVDSTADDR = 7
SOL_IPV6 = 41
IPV6_RECVDSTADDR = 74

def recv_udp(listener, bufsize):
    debug3('Accept UDP python using recvmsg.')
    data, ancdata, _, srcip = listener.recvmsg(4096,
                                               socket.CMSG_SPACE(4))
    dstip = None
    for cmsg_level, cmsg_type, cmsg_data in ancdata:
        if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
            port = 53
            ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
            dstip = (ip, port)
            break
    return (srcip, dstip, data)

def ipfw_rule_exists(n):
    argv = ['ipfw', 'list', '%d' % n]
    p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())

    found = False
    for line in p.stdout:
        if line.startswith(b'%05d ' % n):
            if 'check-state :sshuttle' not in line:
                log('non-sshuttle ipfw rule: %r' % line.strip())
                raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
            found = True
            break
    rv = p.wait()
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
    return found

_oldctls = {}

def _fill_oldctls(prefix):
    argv = ['sysctl', prefix]
    p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
    for line in p.stdout:
        line = line.decode()
        assert(line[-1] == '\n')
        (k, v) = line[:-1].split(': ', 1)
        _oldctls[k] = v.strip()
    rv = p.wait()
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
    if not line:
        raise Fatal('%r returned no data' % (argv,))

def _sysctl_set(name, val):
    argv = ['sysctl', '-w', '%s=%s' % (name, val)]
    debug1('>> %s' % ' '.join(argv))
    return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
    # No env: No output. (Or error that won't be parsed.)

_changedctls = []

def sysctl_set(name, val, permanent=False):
    PREFIX = 'net.inet.ip'
    assert(name.startswith(PREFIX + '.'))
    val = str(val)
    if not _oldctls:
        _fill_oldctls(PREFIX)
    if not (name in _oldctls):
        debug1('>> No such sysctl: %r' % name)
        return False
    oldval = _oldctls[name]
    if val != oldval:
        rv = _sysctl_set(name, val)
        if rv == 0 and permanent:
            debug1('>>   ...saving permanently in /etc/sysctl.conf')
            f = open('/etc/sysctl.conf', 'a')
            f.write('\n'
                    '# Added by sshuttle\n'
                    '%s=%s\n' % (name, val))
            f.close()
        else:
            _changedctls.append(name)
        return True

def ipfw(*args):
    argv = ['ipfw', '-q'] + list(args)
    debug1('>> %s' % ' '.join(argv))
    rv = ssubprocess.call(argv, env=get_env())
    # No env: No output. (Or error that won't be parsed.)
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))

def ipfw_noexit(*args):
    argv = ['ipfw', '-q'] + list(args)
    debug1('>> %s' % ' '.join(argv))
    ssubprocess.call(argv, env=get_env())
    # No env: No output. (Or error that won't be parsed.)

class Method(BaseMethod):

    def get_supported_features(self):
        result = super(Method, self).get_supported_features()
        result.ipv6 = False
        result.udp = False  # NOTE: Almost there, kernel patch needed
        result.dns = True
        return result

    def get_tcp_dstip(self, sock):
        return sock.getsockname()

    def recv_udp(self, udp_listener, bufsize):
        srcip, dstip, data = recv_udp(udp_listener, bufsize)
        if not dstip:
            debug1(
                   "-- ignored UDP from %r: "
                   "couldn't determine destination IP address" % (srcip,))
            return None
        return srcip, dstip, data

    def send_udp(self, sock, srcip, dstip, data):
        if not srcip:
            debug1(
               "-- ignored UDP to %r: "
               "couldn't determine source IP address" % (dstip,))
            return

        # debug3('Sending SRC: %r DST: %r' % (srcip, dstip))
        sender = socket.socket(sock.family, socket.SOCK_DGRAM)
        sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
        sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        sender.bind(srcip)
        sender.sendto(data, dstip)
        sender.close()

    def setup_udp_listener(self, udp_listener):
        if udp_listener.v4 is not None:
            udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
        # if udp_listener.v6 is not None:
        #     udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)

    def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
                       user, tmark):
        # IPv6 not supported
        if family not in [socket.AF_INET]:
            raise Exception(
                'Address family "%s" unsupported by ipfw method_name'
                % family_to_string(family))

        # XXX: Any risk from this?
        ipfw_noexit('delete', '1')

        while _changedctls:
            name = _changedctls.pop()
            oldval = _oldctls[name]
            _sysctl_set(name, oldval)

        if subnets or dnsport:
            sysctl_set('net.inet.ip.fw.enable', 1)

        ipfw('add', '1', 'check-state', ':sshuttle')

        ipfw('add', '1', 'skipto', '2',
             'tcp',
             'from', 'any', 'to', 'table(125)')
        ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
             'tcp',
             'from', 'any', 'to', 'table(126)',
             'setup', 'keep-state', ':sshuttle')

        ipfw_noexit('table', '124', 'flush')
        dnscount = 0
        for _, ip in [i for i in nslist if i[0] == family]:
            ipfw('table', '124', 'add', '%s' % (ip))
            dnscount += 1
        if dnscount > 0:
            ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
                 'udp',
                 'from', 'any', 'to', 'table(124)',
                 'keep-state', ':sshuttle')
        ipfw('add', '1', 'allow',
             'udp',
             'from', 'any', 'to', 'any')

        if subnets:
            # create new subnet entries
            for _, swidth, sexclude, snet, fport, lport in sorted(subnets,
                                                                  key=lambda s: s[1],
                                                                  reverse=True):
                if sexclude:
                    ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
                else:
                    ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))

    def restore_firewall(self, port, family, udp, user):
        if family not in [socket.AF_INET]:
            raise Exception(
                'Address family "%s" unsupported by ipfw method'
                % family_to_string(family))

        ipfw_noexit('delete', '1')
        ipfw_noexit('table', '124', 'flush')
        ipfw_noexit('table', '125', 'flush')
        ipfw_noexit('table', '126', 'flush')

    def is_supported(self):
        if which("ipfw"):
            return True
        debug2("ipfw method not supported because 'ipfw' command is "
               "missing.")
        return False
<|endoftext|>


<commit_before>import re
from collections import OrderedDict

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db import models
from django.utils.timezone import now

import bleach
from jinja2 import Markup
from markdown import Markdown

from standup.status.utils import (
    trim_urls,
    week_end as u_week_end,
    week_start as u_week_start,
)
from standup.mdext.nixheaders import NixHeaderExtension

BUG_RE = re.compile(r'(bug #?(\d+))', flags=re.I)
PULL_RE = re.compile(r'((?:pull|pr) #?(\d+))', flags=re.I)
ISSUE_RE = re.compile(r'(issue #?(\d+))', flags=re.I)
USER_RE = re.compile(r'(?<=^|(?<=[^\w\-.]))@([\w-]+)', flags=re.I)
TAG_RE = re.compile(r'(?:^|[^\w\\/])#([a-z][a-z0-9_.-]*)(?:\b|$)', flags=re.I)
MD = Markdown(output_format='html5', extensions=[
    NixHeaderExtension(),
    'nl2br',
    'smart_strong'
])

class Team(models.Model):
    """A team of users in the organization."""
    name = models.CharField(
        max_length=100,
        help_text='Name of the team'
    )
    slug = models.SlugField(unique=True, max_length=100)

    class Meta:
        db_table = 'team'
        ordering = ('name',)

    def __str__(self):
        return self.name

    def __repr__(self):
        return '<Team: [%s]>' % (self.name,)

    def get_absolute_url(self):
        try:
            return reverse('status.team', kwargs={'slug': self.slug})
        except NoReverseMatch:
            return ''

    def statuses(self):
        user_ids = self.users.values_list('id', flat=True)
        return Status.objects.filter(user__pk__in=user_ids, reply_to=None)

    def dictify(self):
        data = OrderedDict()
        data['id'] = self.id
        data['name'] = self.name
        data['slug'] = self.slug
        return data

class StandupUser(models.Model):
    """A standup participant--tied to Django's User model."""
    # Note: User provides "username", "is_superuser", "is_staff" and "email"
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    name = models.CharField(max_length=100, blank=True, null=True)
    slug = models.SlugField(max_length=100, blank=True, null=True, unique=True)
    irc_nick = models.CharField(
        max_length=100, blank=True, null=True, unique=True,
        help_text='IRC nick for this particular user'
    )
    teams = models.ManyToManyField(Team, related_name='users', through='TeamUser')

    class Meta:
        # ordering = ('user__username',)
        db_table = 'user'

    def __str__(self):
        return self.name or self.slug

    def __repr__(self):
        return '<StandupUser: [{}]>'.format(self.slug)

    def get_absolute_url(self):
        try:
            return reverse('status.user', kwargs={'slug': self.slug})
        except NoReverseMatch:
            return ''

    @property
    def email(self):
        return self.user.email

    def dictify(self):
        """Returns an OrderedDict of model attributes"""
        data = OrderedDict()
        data['id'] = self.id
        data['name'] = self.name
        data['slug'] = self.slug
        data['irc_nick'] = self.irc_nick
        # FIXME: Should we be providing email addresses publicly via the api?
        # data['email'] = self.user.email
        data['is_staff'] = self.user.is_staff
        return data

class TeamUser(models.Model):
    team = models.ForeignKey(Team)
    user = models.ForeignKey(StandupUser)

    class Meta:
        db_table = 'team_users'
        unique_together = (('team', 'user'),)

class Project(models.Model):
    """A project that does standups."""

    name = models.CharField(max_length=100, unique=True)
    slug = models.CharField(max_length=100, unique=True)
    color = models.CharField(max_length=6)
    repo_url = models.URLField(max_length=100)

    class Meta:
        db_table = 'project'
        ordering = ('name',)

    def __str__(self):
        return self.name

    def __repr__(self):
        return '<Project: [%s] %s>' % (self.slug, self.name)

    def get_absolute_url(self):
        try:
            return reverse('status.project', kwargs={'slug': self.slug})
        except NoReverseMatch:
            return ''

    def dictify(self):
        """Returns an OrderedDict of model attributes"""
        data = OrderedDict()
        data['id'] = self.id
        data['name'] = self.name
        data['slug'] = self.slug
        data['color'] = self.color
        return data

class Status(models.Model):
    """A standup update for a user on a given project."""

    created = models.DateTimeField(default=now)
    user = models.ForeignKey(StandupUser, related_name='statuses')
    project = models.ForeignKey('Project', related_name='statuses', null=True, blank=True)
    content = models.TextField()
    content_html = models.TextField()
    reply_to = models.ForeignKey(
        'self', blank=True, null=True, default=None,
        on_delete=models.SET_DEFAULT)

    class Meta:
        db_table = 'status'
        ordering = ('-created',)

    def __str__(self):
        return 'Status from %s' % self.user.slug

    def __repr__(self):
        return '<Status: %s: %s>' % (self.user.slug, self.content)

    def get_absolute_url(self):
        try:
            return reverse('status.status', kwargs={'pk': self.pk})
        except NoReverseMatch:
            return ''

    def replies(self):
        return Status.objects.filter(reply_to=self).order_by('-created')

    @property
    def reply_count(self):
        return self.replies().count()

    @property
    def week_start(self):
        if self.created:
            return u_week_start(self.created)
        return None

    @property
    def week_end(self):
        if self.created:
            return u_week_end(self.created)
        return None

    def htmlify(self):
        # Remove icky stuff.
        formatted = bleach.clean(self.content, tags=[])

        # Linkify "bug #n" and "bug n" text.
        formatted = BUG_RE.sub(
            r'\1',
            formatted)

        # Wrap tags in a span for formatting
        checked = set()
        for tag in TAG_RE.findall(formatted):
            if tag in checked:
                continue
            hashtag = '#%s' % tag
            formatted = formatted.replace(hashtag,
                                          '%s' %
                                          (tag.lower(), hashtag))
            checked.add(tag)

        checked = set()
        for slug in USER_RE.findall(formatted):
            if slug in checked:
                continue
            slug = slug.lstrip('@')
            user = StandupUser.objects.filter(slug=slug).first()
            if user:
                try:
                    url = reverse('status.user', kwargs={'slug': slug})
                except NoReverseMatch:
                    continue
                at_slug = '@%s' % slug
                formatted = formatted.replace(at_slug,
                                              '%s' %
                                              (url, at_slug))
            checked.add(slug)

        # Linkify "pull #n" and "pull n" text.
        if self.project and self.project.repo_url:
            repo = self.project.repo_url.rstrip('/')
            formatted = PULL_RE.sub(
                r'\1' % repo, formatted)
            formatted = ISSUE_RE.sub(
                r'\1' % repo, formatted)

        # markdownify
        formatted = MD.reset().convert(formatted)

        # Linkify bare urls
        formatted = bleach.linkify(formatted, [trim_urls])
        return Markup(formatted)

    def dictify(self):
        """Returns an OrderedDict of model attributes"""
        if self.reply_to:
            reply_to_user_id = self.reply_to.user.id
            reply_to_username = self.reply_to.user.slug
        else:
            reply_to_user_id = None
            reply_to_username = None

        data = OrderedDict()
        data['id'] = self.id
        data['created'] = self.created.isoformat()
        if self.user:
            data['user'] = self.user.dictify()
        else:
            data['user'] = None
        if self.project:
            data['project'] = self.project.dictify()
        else:
            data['project'] = None
        data['content'] = self.htmlify()
        data['reply_to_id'] = self.reply_to.id
        data['reply_to_user_id'] = reply_to_user_id
        data['reply_to_username'] = reply_to_username
        data['reply_count'] = self.reply_count

        # FIXME: What do we need these for?
        # data['week_start'] = self.week_start.strftime("%Y-%m-%d")
        # data['week_end'] = self.week_end.strftime("%Y-%m-%d")

        return data
<commit_msg>Fix Status plural<commit_after>import re
from collections import OrderedDict

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse, NoReverseMatch
from django.db import models
from django.utils.timezone import now

import bleach
from jinja2 import Markup
from markdown import Markdown

from standup.status.utils import (
    trim_urls,
    week_end as u_week_end,
    week_start as u_week_start,
)
from standup.mdext.nixheaders import NixHeaderExtension

BUG_RE = re.compile(r'(bug #?(\d+))', flags=re.I)
PULL_RE = re.compile(r'((?:pull|pr) #?(\d+))', flags=re.I)
ISSUE_RE = re.compile(r'(issue #?(\d+))', flags=re.I)
USER_RE = re.compile(r'(?<=^|(?<=[^\w\-.]))@([\w-]+)', flags=re.I)
TAG_RE = re.compile(r'(?:^|[^\w\\/])#([a-z][a-z0-9_.-]*)(?:\b|$)', flags=re.I)
MD = Markdown(output_format='html5', extensions=[
    NixHeaderExtension(),
    'nl2br',
    'smart_strong'
])

class Team(models.Model):
    """A team of users in the organization."""
    name = models.CharField(
        max_length=100,
        help_text='Name of the team'
    )
    slug = models.SlugField(unique=True, max_length=100)

    class Meta:
        db_table = 'team'
        ordering = ('name',)

    def __str__(self):
        return self.name

    def __repr__(self):
        return '<Team: [%s]>' % (self.name,)

    def get_absolute_url(self):
        try:
            return reverse('status.team', kwargs={'slug': self.slug})
        except NoReverseMatch:
            return ''

    def statuses(self):
        user_ids = self.users.values_list('id', flat=True)
        return Status.objects.filter(user__pk__in=user_ids, reply_to=None)

    def dictify(self):
        data = OrderedDict()
        data['id'] = self.id
        data['name'] = self.name
        data['slug'] = self.slug
        return data

class StandupUser(models.Model):
    """A standup participant--tied to Django's User model."""
    # Note: User provides "username", "is_superuser", "is_staff" and "email"
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    name = models.CharField(max_length=100, blank=True, null=True)
    slug = models.SlugField(max_length=100, blank=True, null=True, unique=True)
    irc_nick = models.CharField(
        max_length=100, blank=True, null=True, unique=True,
        help_text='IRC nick for this particular user'
    )
    teams = models.ManyToManyField(Team, related_name='users', through='TeamUser')

    class Meta:
        # ordering = ('user__username',)
        db_table = 'user'

    def __str__(self):
        return self.name or self.slug

    def __repr__(self):
        return '<StandupUser: [{}]>'.format(self.slug)

    def get_absolute_url(self):
        try:
            return reverse('status.user', kwargs={'slug': self.slug})
        except NoReverseMatch:
            return ''

    @property
    def email(self):
        return self.user.email

    def dictify(self):
        """Returns an OrderedDict of model attributes"""
        data = OrderedDict()
        data['id'] = self.id
        data['name'] = self.name
        data['slug'] = self.slug
        data['irc_nick'] = self.irc_nick
        # FIXME: Should we be providing email addresses publicly via the api?
        # data['email'] = self.user.email
        data['is_staff'] = self.user.is_staff
        return data

class TeamUser(models.Model):
    team = models.ForeignKey(Team)
    user = models.ForeignKey(StandupUser)

    class Meta:
        db_table = 'team_users'
        unique_together = (('team', 'user'),)

class Project(models.Model):
    """A project that does standups."""

    name = models.CharField(max_length=100, unique=True)
    slug = models.CharField(max_length=100, unique=True)
    color = models.CharField(max_length=6)
    repo_url = models.URLField(max_length=100)

    class Meta:
        db_table = 'project'
        ordering = ('name',)

    def __str__(self):
        return self.name

    def __repr__(self):
        return '<Project: [%s] %s>' % (self.slug, self.name)

    def get_absolute_url(self):
        try:
            return reverse('status.project', kwargs={'slug': self.slug})
        except NoReverseMatch:
            return ''

    def dictify(self):
        """Returns an OrderedDict of model attributes"""
        data = OrderedDict()
        data['id'] = self.id
        data['name'] = self.name
        data['slug'] = self.slug
        data['color'] = self.color
        return data

class Status(models.Model):
    """A standup update for a user on a given project."""

    created = models.DateTimeField(default=now)
    user = models.ForeignKey(StandupUser, related_name='statuses')
    project = models.ForeignKey('Project', related_name='statuses', null=True, blank=True)
    content = models.TextField()
    content_html = models.TextField()
    reply_to = models.ForeignKey(
        'self', blank=True, null=True, default=None,
        on_delete=models.SET_DEFAULT)

    class Meta:
        db_table = 'status'
        ordering = ('-created',)
        verbose_name_plural = 'statuses'

    def __str__(self):
        return 'Status from %s' % self.user.slug

    def __repr__(self):
        return '<Status: %s: %s>' % (self.user.slug, self.content)

    def get_absolute_url(self):
        try:
            return reverse('status.status', kwargs={'pk': self.pk})
        except NoReverseMatch:
            return ''

    def replies(self):
        return Status.objects.filter(reply_to=self).order_by('-created')

    @property
    def reply_count(self):
        return self.replies().count()

    @property
    def week_start(self):
        if self.created:
            return u_week_start(self.created)
        return None

    @property
    def week_end(self):
        if self.created:
            return u_week_end(self.created)
        return None

    def htmlify(self):
        # Remove icky stuff.
        formatted = bleach.clean(self.content, tags=[])

        # Linkify "bug #n" and "bug n" text.
        formatted = BUG_RE.sub(
            r'\1',
            formatted)

        # Wrap tags in a span for formatting
        checked = set()
        for tag in TAG_RE.findall(formatted):
            if tag in checked:
                continue
            hashtag = '#%s' % tag
            formatted = formatted.replace(hashtag,
                                          '%s' %
                                          (tag.lower(), hashtag))
            checked.add(tag)

        checked = set()
        for slug in USER_RE.findall(formatted):
            if slug in checked:
                continue
            slug = slug.lstrip('@')
            user = StandupUser.objects.filter(slug=slug).first()
            if user:
                try:
                    url = reverse('status.user', kwargs={'slug': slug})
                except NoReverseMatch:
                    continue
                at_slug = '@%s' % slug
                formatted = formatted.replace(at_slug,
                                              '%s' %
                                              (url, at_slug))
            checked.add(slug)

        # Linkify "pull #n" and "pull n" text.
        if self.project and self.project.repo_url:
            repo = self.project.repo_url.rstrip('/')
            formatted = PULL_RE.sub(
                r'\1' % repo, formatted)
            formatted = ISSUE_RE.sub(
                r'\1' % repo, formatted)

        # markdownify
        formatted = MD.reset().convert(formatted)

        # Linkify bare urls
        formatted = bleach.linkify(formatted, [trim_urls])
        return Markup(formatted)

    def dictify(self):
        """Returns an OrderedDict of model attributes"""
        if self.reply_to:
            reply_to_user_id = self.reply_to.user.id
            reply_to_username = self.reply_to.user.slug
        else:
            reply_to_user_id = None
            reply_to_username = None

        data = OrderedDict()
        data['id'] = self.id
        data['created'] = self.created.isoformat()
        if self.user:
            data['user'] = self.user.dictify()
        else:
            data['user'] = None
        if self.project:
            data['project'] = self.project.dictify()
        else:
            data['project'] = None
        data['content'] = self.htmlify()
        data['reply_to_id'] = self.reply_to.id
        data['reply_to_user_id'] = reply_to_user_id
        data['reply_to_username'] = reply_to_username
        data['reply_count'] = self.reply_count

        # FIXME: What do we need these for?
        # data['week_start'] = self.week_start.strftime("%Y-%m-%d")
        # data['week_end'] = self.week_end.strftime("%Y-%m-%d")

        return data
<|endoftext|>