<commit_before># Copyright (c) 2016-2017 Adobe Systems Incorporated.  All rights reserved.
#
# 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.

import argparse
import config
import datetime
import logging
import os
import re
import sys

import user_sync.config
import user_sync.error
import user_sync.lockfile
import user_sync.rules
import user_sync.connector.directory
import user_sync.connector.dashboard
from user_sync.version import __version__ as APP_VERSION

LOG_STRING_FORMAT = '%(asctime)s %(process)d %(levelname)s %(name)s - %(message)s'
LOG_DATE_FORMAT ='%Y-%m-%d %H:%M:%S'

def process_args():
    parser = argparse.ArgumentParser(description='Adobe Enterprise Dashboard User Sync')
    parser.add_argument('-v', '--version',
                        action='version',
                        version='%(prog)s ' + APP_VERSION)
    parser.add_argument('-t', '--test-mode',
                        help='run API action calls in test mode (does not execute changes). Logs what would have been executed.',
                        action='store_true', dest='test_mode')
    parser.add_argument('-c', '--config-path',
                        help='specify path to config files. (default: "%(default)s")',
                        default=config.DEFAULT_CONFIG_DIRECTORY, metavar='path', dest='config_path')
    parser.add_argument('--config-filename',
                        help='main config filename. (default: "%(default)s")',
                        default=config.DEFAULT_MAIN_CONFIG_FILENAME, metavar='filename', dest='config_filename')
    parser.add_argument('--users',
                        help="specify the users to be considered for sync. Legal values are 'all' (the default), 'group name or names' (one or more specified AD groups), 'file f' (a specified input file).",
                        nargs="*", metavar=('all|file|group', 'arg1'), dest='users')
    parser.add_argument('--user-filter',
                        help='limit the selected set of users that may be examined for syncing, with the pattern being a regular expression.',
                        metavar='pattern', dest='username_filter_pattern')
    parser.add_argument('--source-filter',
                        help='send the file to the specified connector (for example, --source-filter ldap:foo.yml). This parameter is used to limit the scope of the LDAP query.',
                        metavar='connector:file', dest='source_filter_args')
    parser.add_argument('--update-user-info',
                        help="if user information differs between the customer side and the Adobe side, the Adobe side is updated to match.",
                        action='store_true', dest='update_user_info')
    parser.add_argument('--process-groups',
                        help="if the membership in mapped groups differs between the customer side and the Adobe side, the group membership is updated on the Adobe side so that the memberships in mapped groups matches the customer side.",
                        action='store_true', dest='manage_groups')
    parser.add_argument('--remove-nonexistent-users',
                        help='Causes the user sync tool to remove Federated users that exist on the Adobe side if they are not in the customer side AD. This has the effect of deleting the user account if that account is owned by the organization under which the sync operation is being run.',
                        action='store_true', dest='remove_nonexistent_users')
    parser.add_argument('--generate-remove-list',
                        help='processing similar to --remove-nonexistent-users except that rather than performing removals, a file is generated (with the given pathname) listing users who would be removed. This file can then be given in the --remove-list argument in a subsequent run.',
                        metavar='output_path', dest='remove_list_output_path')
    parser.add_argument('-d', '--remove-list',
                        help='specifies the file containing the list of users to be removed. Users on this list are removeFromOrg\'ed on the Adobe side.',
                        metavar='input_path', dest='remove_list_input_path')
    return parser.parse_args()

def init_console_log():
    console_log_handler = logging.StreamHandler(sys.stdout)
    console_log_handler.setFormatter(logging.Formatter(LOG_STRING_FORMAT, LOG_DATE_FORMAT))
    root_logger = logging.getLogger()
    root_logger.addHandler(console_log_handler)
    root_logger.setLevel(logging.DEBUG)
    return console_log_handler

def init_log(logging_config):
    '''
    :type logging_config: user_sync.config.DictConfig
    '''
    builder = user_sync.config.OptionsBuilder(logging_config)
    builder.set_bool_value('log_to_file', False)
    builder.set_string_value('file_log_directory', 'logs')
    builder.set_string_value('file_log_level', 'debug')
    builder.set_string_value('console_log_level', None)
    options = builder.get_options()

    level_lookup = {
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warning': logging.WARNING,
        'error': logging.ERROR,
        'critical': logging.CRITICAL
    }

    console_log_level = level_lookup.get(options['console_log_level'])
    if (console_log_level == None):
        console_log_level = logging.DEBUG
        logger.log(logging.INFO, 'Unknown console log level: %s setting to debug' % options['console_log_level'])
    console_log_handler.setLevel(console_log_level)

    if options['log_to_file'] == True:
        file_log_level = level_lookup.get(options['file_log_level'])
        if (file_log_level == None):
            file_log_level = logging.DEBUG
            logger.log(logging.INFO, 'Unknown file log level: %s setting to debug' % options['file_log_level'])
        file_log_directory = options['file_log_directory']
        if not os.path.exists(file_log_directory):
            os.makedirs(file_log_directory)

        file_path = os.path.join(file_log_directory, datetime.date.today().isoformat() + ".log")
        fileHandler = logging.FileHandler(file_path)
        fileHandler.setLevel(file_log_level)
        fileHandler.setFormatter(logging.Formatter(LOG_STRING_FORMAT, LOG_DATE_FORMAT))
        logging.getLogger().addHandler(fileHandler)

def begin_work(config_loader):
    '''
    :type config_loader: user_sync.config.ConfigLoader
    '''

    directory_groups = config_loader.get_directory_groups()
    owning_dashboard_config = config_loader.get_dashboard_options_for_owning()
    trustee_dashboard_configs = config_loader.get_dashboard_options_for_trustees()
    rule_config = config_loader.get_rule_options()

    referenced_organization_names = set()
    for groups in directory_groups.itervalues():
        for group in groups:
            organization_name = group.organization_name
            if (organization_name != user_sync.rules.OWNING_ORGANIZATION_NAME):
                referenced_organization_names.add(organization_name)
    referenced_organization_names.difference_update(trustee_dashboard_configs.iterkeys())

    if (len(referenced_organization_names) > 0):
        raise user_sync.error.AssertionException('dashboard_groups have references to unknown trustee dashboards: %s' % referenced_organization_names)

    directory_connector = None
    directory_connector_options = None
    directory_connector_module_name = config_loader.get_directory_connector_module_name()
    if (directory_connector_module_name != None):
        directory_connector_module = __import__(directory_connector_module_name, fromlist=[''])
        directory_connector = user_sync.connector.directory.DirectoryConnector(directory_connector_module)
        directory_connector_options = config_loader.get_directory_connector_options(directory_connector.name)

    config_loader.check_unused_config_keys()

    if (directory_connector != None and directory_connector_options != None):
        directory_connector.initialize(directory_connector_options)

    dashboard_owning_connector = user_sync.connector.dashboard.DashboardConnector("owning", owning_dashboard_config)
    dashboard_trustee_connectors = {}
    for trustee_organization_name, trustee_config in trustee_dashboard_configs.iteritems():
        dashboard_trustee_conector = user_sync.connector.dashboard.DashboardConnector("trustee.%s" % trustee_organization_name, trustee_config)
        dashboard_trustee_connectors[trustee_organization_name] = dashboard_trustee_conector
    dashboard_connectors = user_sync.rules.DashboardConnectors(dashboard_owning_connector, dashboard_trustee_connectors)

    rule_processor = user_sync.rules.RuleProcessor(rule_config)
    if (len(directory_groups) == 0 and rule_processor.will_manage_groups()):
        logger.warn('no groups mapped in config file')
    rule_processor.run(directory_groups, directory_connector, dashboard_connectors)

def create_config_loader(args):
    config_bootstrap_options = {
        'config_directory': args.config_path,
        'main_config_filename': args.config_filename,
    }
    config_loader = user_sync.config.ConfigLoader(config_bootstrap_options)
    return config_loader

def create_config_loader_options(args):
    config_options = {
        'test_mode': args.test_mode,
        'manage_groups': args.manage_groups,
        'update_user_info': args.update_user_info,
    }

    users_args = args.users
    if (users_args != None):
        users_action = None if len(users_args) == 0 else user_sync.helper.normalize_string(users_args.pop(0))
        if (users_action == None or users_action == 'all'):
            config_options['directory_connector_module_name'] = 'user_sync.connector.directory_ldap'
        elif (users_action == 'file'):
            if (len(users_args) == 0):
                raise user_sync.error.AssertionException('Missing file path for --users %s [file_path]' % users_action)
            config_options['directory_connector_module_name'] = 'user_sync.connector.directory_csv'
            config_options['directory_connector_overridden_options'] = {'file_path': users_args.pop(0)}
        elif (users_action == 'group'):
            if (len(users_args) == 0):
                raise user_sync.error.AssertionException('Missing groups for --users %s [groups]' % users_action)
            config_options['directory_connector_module_name'] = 'user_sync.connector.directory_ldap'
            config_options['directory_group_filter'] = users_args.pop(0).split(',')
        else:
            raise user_sync.error.AssertionException('Unknown argument --users %s' % users_action)

    username_filter_pattern = args.username_filter_pattern
    if (username_filter_pattern):
        try:
            compiled_expression = re.compile(username_filter_pattern, re.IGNORECASE)
        except Exception as e:
            raise user_sync.error.AssertionException("Bad regular expression for --user-filter: %s reason: %s" % (username_filter_pattern, e.message))
        config_options['username_filter_regex'] = compiled_expression

    remove_list_input_path = args.remove_list_input_path
    if (remove_list_input_path != None):
        logger.info('Reading remove list from: %s', remove_list_input_path)
        remove_user_key_list = user_sync.rules.RuleProcessor.read_remove_list(remove_list_input_path, logger = logger)
        logger.info('Total users in remove list: %d', len(remove_user_key_list))
        config_options['remove_user_key_list'] = remove_user_key_list

    config_options['remove_list_output_path'] = remove_list_output_path = args.remove_list_output_path
    remove_nonexistent_users = args.remove_nonexistent_users
    if (remove_nonexistent_users and remove_list_output_path):
        remove_nonexistent_users = False
        logger.warn('--remove-nonexistent-users ignored when --generate-remove-list is specified')
    config_options['remove_nonexistent_users'] = remove_nonexistent_users

    source_filter_args = args.source_filter_args
    if (source_filter_args != None):
        source_filter_args_separator_index = source_filter_args.find(':')
        if (source_filter_args_separator_index >= 0):
            connector_name = source_filter_args[:source_filter_args_separator_index]
            source_filter_file_path = source_filter_args[source_filter_args_separator_index + 1:]
            config_options['directory_source_filters'] = {
                connector_name: source_filter_file_path
            }
        else:
            raise user_sync.error.AssertionException("Invalid arg for --source-filter: %s" % source_filter_args)

    return config_options

def main():
    run_stats = None
    try:
        try:
            args = process_args()
        except SystemExit:
            return

        config_loader = create_config_loader(args)
        init_log(config_loader.get_logging_config())

        run_stats = user_sync.helper.JobStats("Run", divider = "=")
        run_stats.log_start(logger)

        script_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
        lock_path = os.path.join(script_dir, 'lockfile')
        lock = user_sync.lockfile.ProcessLock(lock_path)
        if lock.set_lock():
            try:
                config_options = create_config_loader_options(args)
                config_loader.set_options(config_options)

                begin_work(config_loader)
            finally:
                lock.unlock()
        else:
            logger.info("Process is already locked")

    except user_sync.error.AssertionException as e:
        if (not e.is_reported()):
            logger.error(e.message)
            e.set_reported()
    except:
        try:
            logger.error('Unhandled exception', exc_info=sys.exc_info())
        except:
            pass

    finally:
        if (run_stats != None):
            run_stats.log_end(logger)

console_log_handler = init_console_log()
logger = logging.getLogger('main')

if __name__ == '__main__':
    main()

<commit_msg>Changed log regarding incorrect log level from info to warning<commit_after># Copyright (c) 2016-2017 Adobe Systems Incorporated.  All rights reserved.
#
# 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.

import argparse
import config
import datetime
import logging
import os
import re
import sys

import user_sync.config
import user_sync.error
import user_sync.lockfile
import user_sync.rules
import user_sync.connector.directory
import user_sync.connector.dashboard
from user_sync.version import __version__ as APP_VERSION

LOG_STRING_FORMAT = '%(asctime)s %(process)d %(levelname)s %(name)s - %(message)s'
LOG_DATE_FORMAT ='%Y-%m-%d %H:%M:%S'

def process_args():
    parser = argparse.ArgumentParser(description='Adobe Enterprise Dashboard User Sync')
    parser.add_argument('-v', '--version',
                        action='version',
                        version='%(prog)s ' + APP_VERSION)
    parser.add_argument('-t', '--test-mode',
                        help='run API action calls in test mode (does not execute changes). Logs what would have been executed.',
                        action='store_true', dest='test_mode')
    parser.add_argument('-c', '--config-path',
                        help='specify path to config files. (default: "%(default)s")',
                        default=config.DEFAULT_CONFIG_DIRECTORY, metavar='path', dest='config_path')
    parser.add_argument('--config-filename',
                        help='main config filename. (default: "%(default)s")',
                        default=config.DEFAULT_MAIN_CONFIG_FILENAME, metavar='filename', dest='config_filename')
    parser.add_argument('--users',
                        help="specify the users to be considered for sync. Legal values are 'all' (the default), 'group name or names' (one or more specified AD groups), 'file f' (a specified input file).",
                        nargs="*", metavar=('all|file|group', 'arg1'), dest='users')
    parser.add_argument('--user-filter',
                        help='limit the selected set of users that may be examined for syncing, with the pattern being a regular expression.',
                        metavar='pattern', dest='username_filter_pattern')
    parser.add_argument('--source-filter',
                        help='send the file to the specified connector (for example, --source-filter ldap:foo.yml). This parameter is used to limit the scope of the LDAP query.',
                        metavar='connector:file', dest='source_filter_args')
    parser.add_argument('--update-user-info',
                        help="if user information differs between the customer side and the Adobe side, the Adobe side is updated to match.",
                        action='store_true', dest='update_user_info')
    parser.add_argument('--process-groups',
                        help="if the membership in mapped groups differs between the customer side and the Adobe side, the group membership is updated on the Adobe side so that the memberships in mapped groups matches the customer side.",
                        action='store_true', dest='manage_groups')
    parser.add_argument('--remove-nonexistent-users',
                        help='Causes the user sync tool to remove Federated users that exist on the Adobe side if they are not in the customer side AD. This has the effect of deleting the user account if that account is owned by the organization under which the sync operation is being run.',
                        action='store_true', dest='remove_nonexistent_users')
    parser.add_argument('--generate-remove-list',
                        help='processing similar to --remove-nonexistent-users except that rather than performing removals, a file is generated (with the given pathname) listing users who would be removed. This file can then be given in the --remove-list argument in a subsequent run.',
                        metavar='output_path', dest='remove_list_output_path')
    parser.add_argument('-d', '--remove-list',
                        help='specifies the file containing the list of users to be removed. Users on this list are removeFromOrg\'ed on the Adobe side.',
                        metavar='input_path', dest='remove_list_input_path')
    return parser.parse_args()

def init_console_log():
    console_log_handler = logging.StreamHandler(sys.stdout)
    console_log_handler.setFormatter(logging.Formatter(LOG_STRING_FORMAT, LOG_DATE_FORMAT))
    root_logger = logging.getLogger()
    root_logger.addHandler(console_log_handler)
    root_logger.setLevel(logging.DEBUG)
    return console_log_handler

def init_log(logging_config):
    '''
    :type logging_config: user_sync.config.DictConfig
    '''
    builder = user_sync.config.OptionsBuilder(logging_config)
    builder.set_bool_value('log_to_file', False)
    builder.set_string_value('file_log_directory', 'logs')
    builder.set_string_value('file_log_level', 'debug')
    builder.set_string_value('console_log_level', None)
    options = builder.get_options()

    level_lookup = {
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warning': logging.WARNING,
        'error': logging.ERROR,
        'critical': logging.CRITICAL
    }

    console_log_level = level_lookup.get(options['console_log_level'])
    if (console_log_level == None):
        console_log_level = logging.DEBUG
        logger.log(logging.INFO, 'Unknown console log level: %s setting to debug' % options['console_log_level'])
    console_log_handler.setLevel(console_log_level)

    if options['log_to_file'] == True:
        file_log_level = level_lookup.get(options['file_log_level'])
        if (file_log_level == None):
            file_log_level = logging.DEBUG
            logger.log(logging.WARNING, 'Unknown file log level: %s setting to debug' % options['file_log_level'])
        file_log_directory = options['file_log_directory']
        if not os.path.exists(file_log_directory):
            os.makedirs(file_log_directory)

        file_path = os.path.join(file_log_directory, datetime.date.today().isoformat() + ".log")
        fileHandler = logging.FileHandler(file_path)
        fileHandler.setLevel(file_log_level)
        fileHandler.setFormatter(logging.Formatter(LOG_STRING_FORMAT, LOG_DATE_FORMAT))
        logging.getLogger().addHandler(fileHandler)

def begin_work(config_loader):
    '''
    :type config_loader: user_sync.config.ConfigLoader
    '''

    directory_groups = config_loader.get_directory_groups()
    owning_dashboard_config = config_loader.get_dashboard_options_for_owning()
    trustee_dashboard_configs = config_loader.get_dashboard_options_for_trustees()
    rule_config = config_loader.get_rule_options()

    referenced_organization_names = set()
    for groups in directory_groups.itervalues():
        for group in groups:
            organization_name = group.organization_name
            if (organization_name != user_sync.rules.OWNING_ORGANIZATION_NAME):
                referenced_organization_names.add(organization_name)
    referenced_organization_names.difference_update(trustee_dashboard_configs.iterkeys())

    if (len(referenced_organization_names) > 0):
        raise user_sync.error.AssertionException('dashboard_groups have references to unknown trustee dashboards: %s' % referenced_organization_names)

    directory_connector = None
    directory_connector_options = None
    directory_connector_module_name = config_loader.get_directory_connector_module_name()
    if (directory_connector_module_name != None):
        directory_connector_module = __import__(directory_connector_module_name, fromlist=[''])
        directory_connector = user_sync.connector.directory.DirectoryConnector(directory_connector_module)
        directory_connector_options = config_loader.get_directory_connector_options(directory_connector.name)

    config_loader.check_unused_config_keys()

    if (directory_connector != None and directory_connector_options != None):
        directory_connector.initialize(directory_connector_options)

    dashboard_owning_connector = user_sync.connector.dashboard.DashboardConnector("owning", owning_dashboard_config)
    dashboard_trustee_connectors = {}
    for trustee_organization_name, trustee_config in trustee_dashboard_configs.iteritems():
        dashboard_trustee_conector = user_sync.connector.dashboard.DashboardConnector("trustee.%s" % trustee_organization_name, trustee_config)
        dashboard_trustee_connectors[trustee_organization_name] = dashboard_trustee_conector
    dashboard_connectors = user_sync.rules.DashboardConnectors(dashboard_owning_connector, dashboard_trustee_connectors)

    rule_processor = user_sync.rules.RuleProcessor(rule_config)
    if (len(directory_groups) == 0 and rule_processor.will_manage_groups()):
        logger.warn('no groups mapped in config file')
    rule_processor.run(directory_groups, directory_connector, dashboard_connectors)

def create_config_loader(args):
    config_bootstrap_options = {
        'config_directory': args.config_path,
        'main_config_filename': args.config_filename,
    }
    config_loader = user_sync.config.ConfigLoader(config_bootstrap_options)
    return config_loader

def create_config_loader_options(args):
    config_options = {
        'test_mode': args.test_mode,
        'manage_groups': args.manage_groups,
        'update_user_info': args.update_user_info,
    }

    users_args = args.users
    if (users_args != None):
        users_action = None if len(users_args) == 0 else user_sync.helper.normalize_string(users_args.pop(0))
        if (users_action == None or users_action == 'all'):
            config_options['directory_connector_module_name'] = 'user_sync.connector.directory_ldap'
        elif (users_action == 'file'):
            if (len(users_args) == 0):
                raise user_sync.error.AssertionException('Missing file path for --users %s [file_path]' % users_action)
            config_options['directory_connector_module_name'] = 'user_sync.connector.directory_csv'
            config_options['directory_connector_overridden_options'] = {'file_path': users_args.pop(0)}
        elif (users_action == 'group'):
            if (len(users_args) == 0):
                raise user_sync.error.AssertionException('Missing groups for --users %s [groups]' % users_action)
            config_options['directory_connector_module_name'] = 'user_sync.connector.directory_ldap'
            config_options['directory_group_filter'] = users_args.pop(0).split(',')
        else:
            raise user_sync.error.AssertionException('Unknown argument --users %s' % users_action)

    username_filter_pattern = args.username_filter_pattern
    if (username_filter_pattern):
        try:
            compiled_expression = re.compile(username_filter_pattern, re.IGNORECASE)
        except Exception as e:
            raise user_sync.error.AssertionException("Bad regular expression for --user-filter: %s reason: %s" % (username_filter_pattern, e.message))
        config_options['username_filter_regex'] = compiled_expression

    remove_list_input_path = args.remove_list_input_path
    if (remove_list_input_path != None):
        logger.info('Reading remove list from: %s', remove_list_input_path)
        remove_user_key_list = user_sync.rules.RuleProcessor.read_remove_list(remove_list_input_path, logger = logger)
        logger.info('Total users in remove list: %d', len(remove_user_key_list))
        config_options['remove_user_key_list'] = remove_user_key_list

    config_options['remove_list_output_path'] = remove_list_output_path = args.remove_list_output_path
    remove_nonexistent_users = args.remove_nonexistent_users
    if (remove_nonexistent_users and remove_list_output_path):
        remove_nonexistent_users = False
        logger.warn('--remove-nonexistent-users ignored when --generate-remove-list is specified')
    config_options['remove_nonexistent_users'] = remove_nonexistent_users

    source_filter_args = args.source_filter_args
    if (source_filter_args != None):
        source_filter_args_separator_index = source_filter_args.find(':')
        if (source_filter_args_separator_index >= 0):
            connector_name = source_filter_args[:source_filter_args_separator_index]
            source_filter_file_path = source_filter_args[source_filter_args_separator_index + 1:]
            config_options['directory_source_filters'] = {
                connector_name: source_filter_file_path
            }
        else:
            raise user_sync.error.AssertionException("Invalid arg for --source-filter: %s" % source_filter_args)

    return config_options

def main():
    run_stats = None
    try:
        try:
            args = process_args()
        except SystemExit:
            return

        config_loader = create_config_loader(args)
        init_log(config_loader.get_logging_config())

        run_stats = user_sync.helper.JobStats("Run", divider = "=")
        run_stats.log_start(logger)

        script_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
        lock_path = os.path.join(script_dir, 'lockfile')
        lock = user_sync.lockfile.ProcessLock(lock_path)
        if lock.set_lock():
            try:
                config_options = create_config_loader_options(args)
                config_loader.set_options(config_options)

                begin_work(config_loader)
            finally:
                lock.unlock()
        else:
            logger.info("Process is already locked")

    except user_sync.error.AssertionException as e:
        if (not e.is_reported()):
            logger.error(e.message)
            e.set_reported()
    except:
        try:
            logger.error('Unhandled exception', exc_info=sys.exc_info())
        except:
            pass

    finally:
        if (run_stats != None):
            run_stats.log_end(logger)

console_log_handler = init_console_log()
logger = logging.getLogger('main')

if __name__ == '__main__':
    main()

<|endoftext|>


<commit_before>from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _

action_names = {
    ADDITION: _('Addition'),
    DELETION: _('Deletion'),
    CHANGE: _('Change'),
}

class ActionListFilter(admin.SimpleListFilter):
    title = _('Action')
    parameter_name = 'action_flag'

    def lookups(self, request, model_admin):
        return action_names.items()

    def queryset(self, request, queryset):
        if self.value():
            queryset = queryset.filter(action_flag=self.value())
        return queryset

class UserListFilter(admin.SimpleListFilter):
    title = _('staff user')
    parameter_name = 'user'

    def lookups(self, request, model_admin):
        staff = get_user_model().objects.filter(is_staff=True)
        return (
            (s.id, force_text(s))
            for s in staff
        )

    def queryset(self, request, queryset):
        if self.value():
            queryset = queryset.filter(user_id=self.value(), user__is_staff=True)
        return queryset

class LogEntryAdmin(admin.ModelAdmin):
    date_hierarchy = 'action_time'

    readonly_fields = (LogEntry._meta.get_all_field_names() +
                       ['object_link', 'action_description', 'user_link'])

    fieldsets = (
        (_(u'Metadata'), {
            'fields': (
                'action_time',
                'user_link',
                'action_description',
                'object_link',
            )
        }),
        (_(u'Detail'), {
            'fields': (
                'change_message',
                'content_type',
                'object_id',
                'object_repr',
            )
        }),
    )

    list_filter = [
        UserListFilter,
        'content_type',
        ActionListFilter
    ]

    search_fields = [
        'object_repr',
        'change_message'
    ]

    list_display_links = [
        'action_time',
        'change_message',
    ]
    list_display = [
        'action_time',
        'user_link',
        'content_type',
        'object_link',
        'action_description',
        'change_message',
    ]

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return request.user.is_superuser and request.method != 'POST'

    def has_delete_permission(self, request, obj=None):
        return False

    def object_link(self, obj):
        if obj.action_flag == DELETION:
            link = escape(obj.object_repr)
        else:
            ct = obj.content_type
            try:
                link = u'%s' % (
                    reverse(
                        'admin:%s_%s_change' % (ct.app_label, ct.model),
                        args=[obj.object_id]
                    ),
                    escape(obj.object_repr),
                )
            except NoReverseMatch:
                link = escape(obj.object_repr)
        return link
    object_link.allow_tags = True
    object_link.admin_order_field = 'object_repr'
    object_link.short_description = u'object'

    def user_link(self, obj):
        try:
            ct = ContentType.objects.get_for_model(obj.user._meta.model)
            link = u'%s' % (
                reverse(
                    'admin:%s_%s_change' % (ct.app_label, ct.model),
                    args=[obj.user.pk]
                ),
                escape(force_text(obj.user)),
            )
        except NoReverseMatch:
            link = escape(force_text(obj.user))
        return link
    user_link.allow_tags = True
    user_link.admin_order_field = 'user'
    user_link.short_description = u'user'

    def queryset(self, request):
        queryset = super(LogEntryAdmin, self).queryset(request)
        return queryset.prefetch_related('content_type')

    def get_actions(self, request):
        actions = super(LogEntryAdmin, self).get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def action_description(self, obj):
        return action_names[obj.action_flag]
    action_description.short_description = 'Action'

admin.site.register(LogEntry, LogEntryAdmin)
<commit_msg>Avoid using ```get_all_field_names()``` method<commit_after>from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _

action_names = {
    ADDITION: _('Addition'),
    DELETION: _('Deletion'),
    CHANGE: _('Change'),
}

class ActionListFilter(admin.SimpleListFilter):
    title = _('Action')
    parameter_name = 'action_flag'

    def lookups(self, request, model_admin):
        return action_names.items()

    def queryset(self, request, queryset):
        if self.value():
            queryset = queryset.filter(action_flag=self.value())
        return queryset

class UserListFilter(admin.SimpleListFilter):
    title = _('staff user')
    parameter_name = 'user'

    def lookups(self, request, model_admin):
        staff = get_user_model().objects.filter(is_staff=True)
        return (
            (s.id, force_text(s))
            for s in staff
        )

    def queryset(self, request, queryset):
        if self.value():
            queryset = queryset.filter(user_id=self.value(), user__is_staff=True)
        return queryset

class LogEntryAdmin(admin.ModelAdmin):
    date_hierarchy = 'action_time'

    readonly_fields = ([f.name for f in LogEntry._meta.fields] +
                       ['object_link', 'action_description', 'user_link'])

    fieldsets = (
        (_(u'Metadata'), {
            'fields': (
                'action_time',
                'user_link',
                'action_description',
                'object_link',
            )
        }),
        (_(u'Detail'), {
            'fields': (
                'change_message',
                'content_type',
                'object_id',
                'object_repr',
            )
        }),
    )

    list_filter = [
        UserListFilter,
        'content_type',
        ActionListFilter
    ]

    search_fields = [
        'object_repr',
        'change_message'
    ]

    list_display_links = [
        'action_time',
        'change_message',
    ]
    list_display = [
        'action_time',
        'user_link',
        'content_type',
        'object_link',
        'action_description',
        'change_message',
    ]

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return request.user.is_superuser and request.method != 'POST'

    def has_delete_permission(self, request, obj=None):
        return False

    def object_link(self, obj):
        if obj.action_flag == DELETION:
            link = escape(obj.object_repr)
        else:
            ct = obj.content_type
            try:
                link = u'%s' % (
                    reverse(
                        'admin:%s_%s_change' % (ct.app_label, ct.model),
                        args=[obj.object_id]
                    ),
                    escape(obj.object_repr),
                )
            except NoReverseMatch:
                link = escape(obj.object_repr)
        return link
    object_link.allow_tags = True
    object_link.admin_order_field = 'object_repr'
    object_link.short_description = u'object'

    def user_link(self, obj):
        try:
            ct = ContentType.objects.get_for_model(obj.user._meta.model)
            link = u'%s' % (
                reverse(
                    'admin:%s_%s_change' % (ct.app_label, ct.model),
                    args=[obj.user.pk]
                ),
                escape(force_text(obj.user)),
            )
        except NoReverseMatch:
            link = escape(force_text(obj.user))
        return link
    user_link.allow_tags = True
    user_link.admin_order_field = 'user'
    user_link.short_description = u'user'

    def queryset(self, request):
        queryset = super(LogEntryAdmin, self).queryset(request)
        return queryset.prefetch_related('content_type')

    def get_actions(self, request):
        actions = super(LogEntryAdmin, self).get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def action_description(self, obj):
        return action_names[obj.action_flag]
    action_description.short_description = 'Action'

admin.site.register(LogEntry, LogEntryAdmin)
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.db import models

from entropy.base import (
    SlugMixin, TitleMixin, TextMixin, ModifiedMixin, CreatedMixin,
    AttributeMixin, EnabledMixin
)

class Page(TitleMixin, SlugMixin, EnabledMixin, TextMixin, CreatedMixin,
           ModifiedMixin, AttributeMixin):

    # title
    # short_title
    # slug
    # enabled
    # text
    # created_at
    # created_by
    # modified_at
    # modified_by
    # attributes

    featured = models.BooleanField(default=False)
    published_at = models.DateField(null=True)

    def get_absolute_url(self):
        """
        Returns the absolute url for a single page instance

        """

        return reverse('pages_detail_page', args=(self.slug, ))

    @staticmethod
    def get_list_url():
        """
        Returns the absolute url for all page objects.
        This is a static method.

        """

        return reverse('pages_all_pages')
<commit_msg>Add Template Mixin<commit_after># -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.db import models

from entropy.base import (
    SlugMixin, TitleMixin, TextMixin, ModifiedMixin, CreatedMixin,
    AttributeMixin, EnabledMixin
)
from templates.mixins import TemplateMixin

class Page(TitleMixin, SlugMixin, EnabledMixin, TextMixin, CreatedMixin,
           ModifiedMixin, AttributeMixin, TemplateMixin):

    # title
    # short_title
    # slug
    # enabled
    # text
    # created_at
    # created_by
    # modified_at
    # modified_by
    # attributes

    featured = models.BooleanField(default=False)
    published_at = models.DateField(null=True)

    def get_absolute_url(self):
        """
        Returns the absolute url for a single page instance

        """

        return reverse('pages_detail_page', args=(self.slug, ))

    @staticmethod
    def get_list_url():
        """
        Returns the absolute url for all page objects.
        This is a static method.

        """

        return reverse('pages_all_pages')
<|endoftext|>


<commit_before>#!/usr/bin/env python

import os
import re
import sys

from parse_header import *

class InvalidNativeArgumentType:
	def __init__(self, type):
		self.type = type

def to_argument_list(args):
	arg_list = []
	for type, name in args:
		yield type + " " + name

def generate_native_code(return_type, name, args, attrs):
	if "$codeless" in attrs:
		return None

	# Write first line, same as function declaration + "{\n".
	code = "SAMPGDK_NATIVE(" + return_type + ", " + name\
		+ "(" + ", ".join(to_argument_list(args)) + ")) {\n"

	# A "static" variable that holds native address.
	if "$real_name" in attrs:
		real_name = attrs["$real_name"]
	else:
		real_name = name
	code += "\tstatic AMX_NATIVE native = sampgdk::Natives::GetInstance().GetNativeWarn(\"" + real_name + "\");\n"

	# Generate code for local variables, params array and ref argument assignment.
	if len(args) > 0:
		locals_code = ""
		params_code = "\tcell params[] = {\n"
		params_code += "\t\t" + str(len(args)) + " * sizeof(cell)"
		assign_code = ""
		expect_buffer_size = False
		for type, name in args:
			# Local FakeAmxHeapObject instances.
			if expect_buffer_size:
				locals_code += name + ");\n"
				assign_code += name + ");\n"
				expect_buffer_size = False
			if type.endswith("*"):
				locals_code += "\tsampgdk::FakeAmxHeapObject " + name + "_"
				if type == "char *":
					# Output string buffer whose size is passed via next argument.
					locals_code += "("
					expect_buffer_size = True
				elif type == "const char *":
					# Constant string.
					locals_code += "(" + name + ");\n"
				else:
					# Other output parameters.
					locals_code += ";\n"
			params_code += ",\n\t\t"
			if type == "int" or type == "bool":
				params_code += name
			elif type == "float":
				params_code += "amx_ftoc(" + name + ")"
			elif type.endswith("*"):
				params_code += name + "_.address()"
			else:
				raise InvalidNativeArgumentType(type)
			# Assignment of pointer arguments.
			if type.endswith("*"):
				if type == "const char *":
					pass
				elif type == "char *":
					assign_code += "\t" + name + "_.GetAsString(" + name + ", "
					expect_buffer_size = True
				else:
					assign_code += "\t*" + name + " = " + name + "_."
					if type == "int *":
						assign_code += "Get();\n"
					elif type == "bool *":
						assign_code += "GetAsBool();\n"
					elif type == "float *":
						assign_code += "GetAsFloat();\n"
					else:
						raise InvalidNativeArgumentType(type)
		params_code += "\n\t};\n"
		code += locals_code + params_code + assign_code

	code += "\treturn sampgdk::FakeAmx::"
	if return_type == "bool":
		code += "CallNativeBool"
	elif return_type == "float":
		code += "CallNativeFloat"
	else:
		code += "CallNative"
	if len(args) > 0:
		code += "(native, params);\n"
	else:
		code += "(native);\n"
	code += "}\n"
	return code

def main(argv):
	for native in get_natives(sys.stdin.read()):
		try:
			code = generate_native_code(*native)
			if code is not None:
				sys.stdout.write(code + "\n")
			sys.stdout.flush()
		except InvalidNativeArgumentType as arg:
			sys.stderr.write("Invalid argument type '" + arg.type + "' in declaration:\n" + line + "\n")

if __name__ == "__main__":
	main(sys.argv)
<commit_msg>Fix pointer arguments being set too early<commit_after>#!/usr/bin/env python

import os
import re
import sys

from parse_header import *

class InvalidNativeArgumentType:
	def __init__(self, type):
		self.type = type

def to_argument_list(args):
	arg_list = []
	for type, name in args:
		yield type + " " + name

def generate_native_code(return_type, name, args, attrs):
	if "$codeless" in attrs:
		return None

	# Write first line, same as function declaration + "{\n".
	code = "SAMPGDK_NATIVE(" + return_type + ", " + name\
		+ "(" + ", ".join(to_argument_list(args)) + ")) {\n"

	# A "static" variable that holds native address.
	if "$real_name" in attrs:
		real_name = attrs["$real_name"]
	else:
		real_name = name
	code += "\tstatic AMX_NATIVE native = sampgdk::Natives::GetInstance().GetNativeWarn(\"" + real_name + "\");\n"

	locals_code = ""
	params_code = ""
	assign_code = ""

	if len(args) > 0:
		params_code += "\tcell params[] = {\n"
		params_code += "\t\t" + str(len(args)) + " * sizeof(cell)"
		expect_buffer_size = False
		for type, name in args:
			# Local FakeAmxHeapObject instances.
			if expect_buffer_size:
				locals_code += name + ");\n"
				assign_code += name + ");\n"
				expect_buffer_size = False
			if type.endswith("*"):
				locals_code += "\tsampgdk::FakeAmxHeapObject " + name + "_"
				if type == "char *":
					# Output string buffer whose size is passed via next argument.
					locals_code += "("
					expect_buffer_size = True
				elif type == "const char *":
					# Constant string.
					locals_code += "(" + name + ");\n"
				else:
					# Other output parameters.
					locals_code += ";\n"
			params_code += ",\n\t\t"
			if type == "int" or type == "bool":
				params_code += name
			elif type == "float":
				params_code += "amx_ftoc(" + name + ")"
			elif type.endswith("*"):
				params_code += name + "_.address()"
			else:
				raise InvalidNativeArgumentType(type)
			# Assignment of pointer arguments.
			if type.endswith("*"):
				if type == "const char *":
					pass
				elif type == "char *":
					assign_code += "\t" + name + "_.GetAsString(" + name + ", "
					expect_buffer_size = True
				else:
					assign_code += "\t*" + name + " = " + name + "_."
					if type == "int *":
						assign_code += "Get();\n"
					elif type == "bool *":
						assign_code += "GetAsBool();\n"
					elif type == "float *":
						assign_code += "GetAsFloat();\n"
					else:
						raise InvalidNativeArgumentType(type)
		params_code += "\n\t};\n"

	code += locals_code + params_code

	if len(assign_code) > 0:
		code += "\t" + return_type + " retval = "
	else:
		code += "\treturn "
	code += "sampgdk::FakeAmx::"
	if return_type == "bool":
		code += "CallNativeBool"
	elif return_type == "float":
		code += "CallNativeFloat"
	else:
		code += "CallNative"
	if len(args) > 0:
		code += "(native, params);\n"
	else:
		code += "(native);\n"
	if len(assign_code) > 0:
		code += assign_code
		code += "\treturn retval;\n"

	code += "}\n"
	return code

def main(argv):
	for native in get_natives(sys.stdin.read()):
		try:
			code = generate_native_code(*native)
			if code is not None:
				sys.stdout.write(code + "\n")
			sys.stdout.flush()
		except InvalidNativeArgumentType as arg:
			sys.stderr.write("Invalid argument type '" + arg.type + "' in declaration:\n" + line + "\n")

if __name__ == "__main__":
	main(sys.argv)
<|endoftext|>


<commit_before>import html
import discord
from dog import Cog, util
from dog.anime import anime_search
from discord.ext import commands

class Anime(Cog):
    def _make_anime_embed(self, anime):
        print(anime)
        embed = discord.Embed(title=anime.title)
        not_airing = anime.end_date == '0000-00-00' or anime.status != 'Finished Airing'
        embed.add_field(name='Score', value=anime.score)
        embed.add_field(name='Episodes', value=anime.episodes)
        embed.add_field(name='Status', value=anime.status)
        if not_airing:
            embed.add_field(name='Start date', value=anime.start_date)
        else:
            embed.add_field(name='Aired', value=f'{anime.start_date} - {anime.end_date}')
        synopsis = html.unescape(anime.synopsis).replace('
', '\n')[:2500]
        embed.add_field(name='Synopsis', value=util.truncate(synopsis, 1000), inline=False)
        return embed

    @commands.command()
    async def anime(self, ctx, *, query: str):
        """ Searches for anime on MyAnimeList. """
        results = (await anime_search(query))[:20]

        if len(results) > 1:
            choices = '\n'.join(f'{idx + 1}) {an.title}' for idx, an in enumerate(results))
            await ctx.send('Too many results! Pick one:\n\n' + choices)
            choice = 0

            while True:
                msg = await self.bot.wait_for_response(ctx)
                try:
                    choice = int(msg.content)
                    break
                except ValueError:
                    await ctx.send('Invalid choice. Try again.')
            await ctx.send(embed=self._make_anime_embed(results[choice - 1]))
        else:
            await ctx.send(embed=self._make_anime_embed(results[0]))

def setup(bot):
    bot.add_cog(Anime(bot))
<commit_msg>anime: thumbnail<commit_after>import html
import discord
from dog import Cog, util
from dog.anime import anime_search
from discord.ext import commands

class Anime(Cog):
    def _make_anime_embed(self, anime):
        print(anime)
        embed = discord.Embed(title=anime.title)
        not_airing = anime.end_date == '0000-00-00' or anime.status != 'Finished Airing'
        embed.add_field(name='Score', value=anime.score)
        embed.add_field(name='Episodes', value=anime.episodes)
        embed.add_field(name='Status', value=anime.status)
        if not_airing:
            embed.add_field(name='Start date', value=anime.start_date)
        else:
            embed.add_field(name='Aired', value=f'{anime.start_date} - {anime.end_date}')
        synopsis = html.unescape(anime.synopsis).replace('
', '\n')[:2500]
        embed.add_field(name='Synopsis', value=util.truncate(synopsis, 1000), inline=False)
        embed.set_thumbnail(url=anime.image)
        return embed

    @commands.command()
    async def anime(self, ctx, *, query: str):
        """ Searches for anime on MyAnimeList. """
        results = (await anime_search(query))[:20]

        if len(results) > 1:
            choices = '\n'.join(f'{idx + 1}) {an.title}' for idx, an in enumerate(results))
            await ctx.send('Too many results! Pick one:\n\n' + choices)
            choice = 0

            while True:
                msg = await self.bot.wait_for_response(ctx)
                try:
                    choice = int(msg.content)
                    break
                except ValueError:
                    await ctx.send('Invalid choice. Try again.')
            await ctx.send(embed=self._make_anime_embed(results[choice - 1]))
        else:
            await ctx.send(embed=self._make_anime_embed(results[0]))

def setup(bot):
    bot.add_cog(Anime(bot))
<|endoftext|>


<commit_before>#!/usr/bin/env python3
"""
A program to plot the users joining a chat over time. Note that leaving events are not noted.
TODO: support multiple chats.
TODO: support saving to image
"""
import argparse
import os
from json import loads
from datetime import date
from collections import defaultdict
from pprint import pprint

import matplotlib.pyplot as plt

def main():
    """
    main function
    """
    parser = argparse.ArgumentParser(
            description="Visualise the growth a chat over time - note that leavers are not recorded, so this program can only show the join rate.")
    parser.add_argument('path', help='the json file (chat log) to analyse')

    args = parser.parse_args()
    path = args.path

    with open(path, 'r') as f:
        events = (loads(line) for line in f)

        counter = defaultdict(int)
        for event in events:
            if "action" in event and (event["action"]["type"] == "chat_add_user" or event['action']['type'] == 'chat_add_user_link'):
                day = date.fromtimestamp(event["date"])
                counter[day] += 1

    _, filename = os.path.split(path)
    filename, _ = os.path.splitext(filename)

    #frequencies = {key: l.count(True)/l.count(False) * 100 for key, l in counter.items()}
    users_per_day = sorted(counter.items())

    u_count = 0
    for idx, (day, users) in enumerate(users_per_day):
        u_count += users
        users_per_day[idx] = (day, u_count)

    print(users_per_day)
    plt.plot(*zip(*users_per_day))
    plt.title('members in "{}"'.format(filename))

    plt.show()

if __name__ == "__main__":
    main()
<commit_msg>minor edit<commit_after>#!/usr/bin/env python3
"""
A program to plot the users joining a chat over time. Note that leaving events are not noted.
TODO: support multiple chats.
TODO: support saving to image
"""
import argparse
import os
from json import loads
from datetime import date
from collections import defaultdict
from pprint import pprint

import matplotlib.pyplot as plt

def main():
    """
    main function
    """
    parser = argparse.ArgumentParser(
            description="Visualise the growth a chat over time - note that leavers are not recorded, so this program can only show the join rate.")
    parser.add_argument('path', help='the json file (chat log) to analyse')

    args = parser.parse_args()
    path = args.path

    with open(path, 'r') as f:
        events = (loads(line) for line in f)

        counter = defaultdict(int)
        for event in events:
            if "action" in event and (event["action"]["type"] == "chat_add_user" or event['action']['type'] == 'chat_add_user_link'):
                day = date.fromtimestamp(event["date"])
                counter[day] += 1

    filename = path.splitext(path.split(filepath)[-1])[0]

    #frequencies = {key: l.count(True)/l.count(False) * 100 for key, l in counter.items()}
    users_per_day = sorted(counter.items())

    u_count = 0
    for idx, (day, users) in enumerate(users_per_day):
        u_count += users
        users_per_day[idx] = (day, u_count)

    print(users_per_day)
    plt.plot(*zip(*users_per_day))
    plt.title('members in "{}"'.format(filename))

    plt.show()

if __name__ == "__main__":
    main()
<|endoftext|>


<commit_before>from django import forms
from django.db import models
from django.http.response import JsonResponse
from modelcluster.fields import ParentalKey
from wagtail.wagtailadmin.edit_handlers import (FieldPanel,
                                                InlinePanel,
                                                StreamFieldPanel)
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.blocks import FieldBlock, RawHTMLBlock, StructBlock
from wagtail.wagtailcore.fields import RichTextField, StreamField
from wagtail.wagtailcore.models import Orderable, Page
from wagtail.wagtailimages.blocks import ImageChooserBlock
from wagtail.wagtaildocs.blocks import DocumentChooserBlock
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from openstax.functions import build_image_url

class ImageFormatChoiceBlock(FieldBlock):
    field = forms.ChoiceField(choices=(
        ('left', 'Wrap left'), ('right', 'Wrap right'), ('mid', 'Mid width'), ('full', 'Full width'),))

class HTMLAlignmentChoiceBlock(FieldBlock):
    field = forms.ChoiceField(
        choices=(('normal', 'Normal'), ('full', 'Full width'),))

class ImageBlock(StructBlock):
    image = ImageChooserBlock(required=False)
    alignment = ImageFormatChoiceBlock()

class AlignedHTMLBlock(StructBlock):
    html = RawHTMLBlock()
    alignment = HTMLAlignmentChoiceBlock()

    class Meta:
        icon = "code"

class StrategicAdvisors(models.Model):
    name = models.CharField(max_length=255, help_text="Strategic Advisor Name")
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    def get_advisor_image(self):
        return build_image_url(self.image)

    advisor_image = property(get_advisor_image)

    description = models.TextField()

    api_fields = ('name', 'advisor_image', 'description', )

    panels = [
        FieldPanel('name'),
        ImageChooserPanel('image'),
        FieldPanel('description'),
    ]

class OpenStaxTeam(models.Model):
    name = models.CharField(max_length=255, help_text="Team Member Name")
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    def get_team_member_image(self):
        return build_image_url(self.image)
    team_member_image = property(get_team_member_image)

    position = models.CharField(max_length=255)
    description = models.TextField()

    api_fields = ('name', 'team_member_image', 'position', 'description', )

    panels = [
        FieldPanel('name'),
        ImageChooserPanel('image'),
        FieldPanel('position'),
        FieldPanel('description'),
    ]

class ColumnBlock(blocks.StructBlock):
    heading = blocks.CharBlock(required=False)
    content = blocks.RichTextBlock(required=False)
    image = ImageBlock(required=False)
    document = DocumentChooserBlock(required=False)
    cta = blocks.CharBlock(required=False)
    link = blocks.URLBlock(required=False)

    class Meta:
        icon = 'placeholder'

class AboutUsStrategicAdvisors(Orderable, StrategicAdvisors):
    page = ParentalKey('pages.AboutUs', related_name='strategic_advisors')

class AboutUsOpenStaxTeam(Orderable, OpenStaxTeam):
    page = ParentalKey('pages.AboutUs', related_name='openstax_team')

class AboutUs(Page):
    tagline = models.CharField(max_length=255)
    intro_heading = models.CharField(max_length=255)
    intro_paragraph = models.TextField()
    our_team_heading = models.CharField(max_length=255)

    api_fields = (
        'tagline',
        'intro_heading',
        'intro_paragraph',
        'our_team_heading',
        'openstax_team',
        'strategic_advisors',
        'slug',
        'seo_title',
        'search_description',)

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('tagline'),
        FieldPanel('intro_heading'),
        FieldPanel('intro_paragraph'),
        FieldPanel('our_team_heading'),
        InlinePanel('openstax_team', label="OpenStax Team"),
        InlinePanel('strategic_advisors', label="Strategic Advisors"),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    parent_page_types = ['pages.HomePage']

class Quote(models.Model):
    IMAGE_ALIGNMENT_CHOICES = (
        ('L', 'left'),
        ('R', 'right'),
        ('F', 'full'),
    )
    quote_text = RichTextField()

    quote_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    def get_quote_image(self):
        return build_image_url(self.quote_image)
    quote_image_url = property(get_quote_image)

    quote_image_alignment = models.CharField(max_length=1,
                                             choices=IMAGE_ALIGNMENT_CHOICES,
                                             blank=True,
                                             null=True)
    quote_link = models.URLField(blank=True, null=True)
    quote_link_text = models.CharField(max_length=255, blank=True, null=True)

    api_fields = (
        'quote_text',
        'quote_image_url',
        'get_quote_image_alignment_display',
        'quote_link',
        'quote_link_text',
    )

    panels = [
        FieldPanel('quote_text'),
        ImageChooserPanel('quote_image'),
        FieldPanel('quote_image_alignment'),
        FieldPanel('quote_link'),
        FieldPanel('quote_link_text'),
    ]

# Home Page
class HomePage(Page):
    row_1 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_2 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_3 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_4 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_5 = StreamField([
        ('column', ColumnBlock()),
    ])

    api_fields = (
        'title',
        'row_1',
        'row_2',
        'row_3',
        'row_4',
        'row_5',
        'slug',
        'seo_title',
        'search_description',)

    class Meta:
        verbose_name = "Home Page"

    content_panels = [
        FieldPanel('title', classname="full title"),
        StreamFieldPanel('row_1'),
        StreamFieldPanel('row_2'),
        StreamFieldPanel('row_3'),
        StreamFieldPanel('row_4'),
        StreamFieldPanel('row_5'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    # we are controlling what types of pages are allowed under a homepage
    # if a new page type is created, it needs to be added here to show up in
    # the admin
    subpage_types = [
        'pages.HigherEducation',
        'pages.ContactUs',
        'pages.AboutUs',
        'pages.GeneralPage',
        'pages.EcosystemAllies',
        'books.BookIndex',
        'news.NewsIndex',
        'allies.Ally',
    ]

class HigherEducation(Page):
    intro_heading = models.CharField(max_length=255)
    intro_description = models.TextField()

    row_1 = StreamField([
        ('column', ColumnBlock()),
    ])

    get_started_heading = models.CharField(max_length=255)

    get_started_step_1_heading = models.CharField(max_length=255)
    get_started_step_1_description = models.TextField()
    get_started_step_1_cta = models.CharField(max_length=255)

    get_started_step_2_heading = models.CharField(max_length=255)
    get_started_step_2_description = models.TextField()
    get_started_step_2_cta = models.CharField(max_length=255)

    get_started_step_3_heading = models.CharField(max_length=255)
    get_started_step_3_description = models.TextField()
    get_started_step_3_cta = models.CharField(max_length=255)

    adopt_heading = models.CharField(max_length=255)
    adopt_description = models.TextField()
    adopt_cta = models.CharField(max_length=255)

    row_2 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_3 = StreamField([
        ('column', ColumnBlock()),
    ])

    api_fields = (
        'intro_heading',
        'intro_description',
        'row_1',
        'get_started_heading',
        'get_started_step_1_heading',
        'get_started_step_1_description',
        'get_started_step_1_cta',
        'get_started_step_2_heading',
        'get_started_step_2_description',
        'get_started_step_2_cta',
        'get_started_step_3_heading',
        'get_started_step_3_description',
        'get_started_step_3_cta',
        'adopt_heading',
        'adopt_description',
        'adopt_cta',
        'row_2',
        'row_3',
        'slug',
        'seo_title',
        'search_description',)

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('intro_heading'),
        FieldPanel('intro_description'),
        StreamFieldPanel('row_1'),
        FieldPanel('get_started_heading'),
        FieldPanel('get_started_step_1_heading'),
        FieldPanel('get_started_step_1_description'),
        FieldPanel('get_started_step_1_cta'),
        FieldPanel('get_started_step_2_heading'),
        FieldPanel('get_started_step_2_description'),
        FieldPanel('get_started_step_2_cta'),
        FieldPanel('get_started_step_3_heading'),
        FieldPanel('get_started_step_3_description'),
        FieldPanel('get_started_step_3_cta'),
        FieldPanel('adopt_heading'),
        FieldPanel('adopt_description'),
        FieldPanel('adopt_cta'),
        StreamFieldPanel('row_2'),
        StreamFieldPanel('row_3'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    parent_page_types = ['pages.HomePage']

class ContactUs(Page):
    tagline = models.CharField(max_length=255)
    mailing_header = models.CharField(max_length=255)
    mailing_address = RichTextField()
    phone_number = models.CharField(max_length=255)

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('tagline'),
        FieldPanel('mailing_header'),
        FieldPanel('mailing_address'),
        FieldPanel('phone_number'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    api_fields = (
        'title',
        'tagline',
        'mailing_header',
        'mailing_address',
        'phone_number',
        'slug',
        'seo_title',
        'search_description',
    )

    parent_page_types = ['pages.HomePage']

class GeneralPage(Page):
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('tagline', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
        ('multicolumn', blocks.StreamBlock([
            ('column', ColumnBlock()),
            ],
            icon='placeholder'
        )),
        ('html', RawHTMLBlock()),
    ])

    api_fields = (
        'title',
        'body',
        'slug',
        'seo_title',
        'search_description',
    )

    content_panels = [
        FieldPanel('title'),
        StreamFieldPanel('body'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    def serve(self, request, *args, **kwargs):
        data = {
            'title': self.title,
            'slug': self.slug,
            'seo_title': self.seo_title,
            'search_description': self.search_description,
            'body': self.body,
        }

        return JsonResponse(data)

class EcosystemAllies(Page):
    page_description = models.TextField()

    api_fields = (
        'title',
        'page_description',
        'slug',
        'seo_title',
        'search_description',
    )

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('page_description'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    parent_page_types = ['pages.HomePage']
<commit_msg>add allies to EcosystemAllies<commit_after>from django import forms
from django.db import models
from django.http.response import JsonResponse
from modelcluster.fields import ParentalKey
from wagtail.wagtailadmin.edit_handlers import (FieldPanel,
                                                InlinePanel,
                                                StreamFieldPanel)
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.blocks import FieldBlock, RawHTMLBlock, StructBlock
from wagtail.wagtailcore.fields import RichTextField, StreamField
from wagtail.wagtailcore.models import Orderable, Page
from wagtail.wagtailimages.blocks import ImageChooserBlock
from wagtail.wagtaildocs.blocks import DocumentChooserBlock
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from openstax.functions import build_image_url

from allies.models import Ally

class ImageFormatChoiceBlock(FieldBlock):
    field = forms.ChoiceField(choices=(
        ('left', 'Wrap left'), ('right', 'Wrap right'), ('mid', 'Mid width'), ('full', 'Full width'),))

class HTMLAlignmentChoiceBlock(FieldBlock):
    field = forms.ChoiceField(
        choices=(('normal', 'Normal'), ('full', 'Full width'),))

class ImageBlock(StructBlock):
    image = ImageChooserBlock(required=False)
    alignment = ImageFormatChoiceBlock()

class AlignedHTMLBlock(StructBlock):
    html = RawHTMLBlock()
    alignment = HTMLAlignmentChoiceBlock()

    class Meta:
        icon = "code"

class StrategicAdvisors(models.Model):
    name = models.CharField(max_length=255, help_text="Strategic Advisor Name")
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    def get_advisor_image(self):
        return build_image_url(self.image)

    advisor_image = property(get_advisor_image)

    description = models.TextField()

    api_fields = ('name', 'advisor_image', 'description', )

    panels = [
        FieldPanel('name'),
        ImageChooserPanel('image'),
        FieldPanel('description'),
    ]

class OpenStaxTeam(models.Model):
    name = models.CharField(max_length=255, help_text="Team Member Name")
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    def get_team_member_image(self):
        return build_image_url(self.image)
    team_member_image = property(get_team_member_image)

    position = models.CharField(max_length=255)
    description = models.TextField()

    api_fields = ('name', 'team_member_image', 'position', 'description', )

    panels = [
        FieldPanel('name'),
        ImageChooserPanel('image'),
        FieldPanel('position'),
        FieldPanel('description'),
    ]

class ColumnBlock(blocks.StructBlock):
    heading = blocks.CharBlock(required=False)
    content = blocks.RichTextBlock(required=False)
    image = ImageBlock(required=False)
    document = DocumentChooserBlock(required=False)
    cta = blocks.CharBlock(required=False)
    link = blocks.URLBlock(required=False)

    class Meta:
        icon = 'placeholder'

class AboutUsStrategicAdvisors(Orderable, StrategicAdvisors):
    page = ParentalKey('pages.AboutUs', related_name='strategic_advisors')

class AboutUsOpenStaxTeam(Orderable, OpenStaxTeam):
    page = ParentalKey('pages.AboutUs', related_name='openstax_team')

class AboutUs(Page):
    tagline = models.CharField(max_length=255)
    intro_heading = models.CharField(max_length=255)
    intro_paragraph = models.TextField()
    our_team_heading = models.CharField(max_length=255)

    api_fields = (
        'tagline',
        'intro_heading',
        'intro_paragraph',
        'our_team_heading',
        'openstax_team',
        'strategic_advisors',
        'slug',
        'seo_title',
        'search_description',)

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('tagline'),
        FieldPanel('intro_heading'),
        FieldPanel('intro_paragraph'),
        FieldPanel('our_team_heading'),
        InlinePanel('openstax_team', label="OpenStax Team"),
        InlinePanel('strategic_advisors', label="Strategic Advisors"),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    parent_page_types = ['pages.HomePage']

class Quote(models.Model):
    IMAGE_ALIGNMENT_CHOICES = (
        ('L', 'left'),
        ('R', 'right'),
        ('F', 'full'),
    )
    quote_text = RichTextField()

    quote_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    def get_quote_image(self):
        return build_image_url(self.quote_image)
    quote_image_url = property(get_quote_image)

    quote_image_alignment = models.CharField(max_length=1,
                                             choices=IMAGE_ALIGNMENT_CHOICES,
                                             blank=True,
                                             null=True)
    quote_link = models.URLField(blank=True, null=True)
    quote_link_text = models.CharField(max_length=255, blank=True, null=True)

    api_fields = (
        'quote_text',
        'quote_image_url',
        'get_quote_image_alignment_display',
        'quote_link',
        'quote_link_text',
    )

    panels = [
        FieldPanel('quote_text'),
        ImageChooserPanel('quote_image'),
        FieldPanel('quote_image_alignment'),
        FieldPanel('quote_link'),
        FieldPanel('quote_link_text'),
    ]

# Home Page
class HomePage(Page):
    row_1 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_2 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_3 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_4 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_5 = StreamField([
        ('column', ColumnBlock()),
    ])

    api_fields = (
        'title',
        'row_1',
        'row_2',
        'row_3',
        'row_4',
        'row_5',
        'slug',
        'seo_title',
        'search_description',)

    class Meta:
        verbose_name = "Home Page"

    content_panels = [
        FieldPanel('title', classname="full title"),
        StreamFieldPanel('row_1'),
        StreamFieldPanel('row_2'),
        StreamFieldPanel('row_3'),
        StreamFieldPanel('row_4'),
        StreamFieldPanel('row_5'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    # we are controlling what types of pages are allowed under a homepage
    # if a new page type is created, it needs to be added here to show up in
    # the admin
    subpage_types = [
        'pages.HigherEducation',
        'pages.ContactUs',
        'pages.AboutUs',
        'pages.GeneralPage',
        'pages.EcosystemAllies',
        'books.BookIndex',
        'news.NewsIndex',
        'allies.Ally',
    ]

class HigherEducation(Page):
    intro_heading = models.CharField(max_length=255)
    intro_description = models.TextField()

    row_1 = StreamField([
        ('column', ColumnBlock()),
    ])

    get_started_heading = models.CharField(max_length=255)

    get_started_step_1_heading = models.CharField(max_length=255)
    get_started_step_1_description = models.TextField()
    get_started_step_1_cta = models.CharField(max_length=255)

    get_started_step_2_heading = models.CharField(max_length=255)
    get_started_step_2_description = models.TextField()
    get_started_step_2_cta = models.CharField(max_length=255)

    get_started_step_3_heading = models.CharField(max_length=255)
    get_started_step_3_description = models.TextField()
    get_started_step_3_cta = models.CharField(max_length=255)

    adopt_heading = models.CharField(max_length=255)
    adopt_description = models.TextField()
    adopt_cta = models.CharField(max_length=255)

    row_2 = StreamField([
        ('column', ColumnBlock()),
    ])
    row_3 = StreamField([
        ('column', ColumnBlock()),
    ])

    api_fields = (
        'intro_heading',
        'intro_description',
        'row_1',
        'get_started_heading',
        'get_started_step_1_heading',
        'get_started_step_1_description',
        'get_started_step_1_cta',
        'get_started_step_2_heading',
        'get_started_step_2_description',
        'get_started_step_2_cta',
        'get_started_step_3_heading',
        'get_started_step_3_description',
        'get_started_step_3_cta',
        'adopt_heading',
        'adopt_description',
        'adopt_cta',
        'row_2',
        'row_3',
        'slug',
        'seo_title',
        'search_description',)

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('intro_heading'),
        FieldPanel('intro_description'),
        StreamFieldPanel('row_1'),
        FieldPanel('get_started_heading'),
        FieldPanel('get_started_step_1_heading'),
        FieldPanel('get_started_step_1_description'),
        FieldPanel('get_started_step_1_cta'),
        FieldPanel('get_started_step_2_heading'),
        FieldPanel('get_started_step_2_description'),
        FieldPanel('get_started_step_2_cta'),
        FieldPanel('get_started_step_3_heading'),
        FieldPanel('get_started_step_3_description'),
        FieldPanel('get_started_step_3_cta'),
        FieldPanel('adopt_heading'),
        FieldPanel('adopt_description'),
        FieldPanel('adopt_cta'),
        StreamFieldPanel('row_2'),
        StreamFieldPanel('row_3'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    parent_page_types = ['pages.HomePage']

class ContactUs(Page):
    tagline = models.CharField(max_length=255)
    mailing_header = models.CharField(max_length=255)
    mailing_address = RichTextField()
    phone_number = models.CharField(max_length=255)

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('tagline'),
        FieldPanel('mailing_header'),
        FieldPanel('mailing_address'),
        FieldPanel('phone_number'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    api_fields = (
        'title',
        'tagline',
        'mailing_header',
        'mailing_address',
        'phone_number',
        'slug',
        'seo_title',
        'search_description',
    )

    parent_page_types = ['pages.HomePage']

class GeneralPage(Page):
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('tagline', blocks.CharBlock(classname="full title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
        ('multicolumn', blocks.StreamBlock([
            ('column', ColumnBlock()),
            ],
            icon='placeholder'
        )),
        ('html', RawHTMLBlock()),
    ])

    api_fields = (
        'title',
        'body',
        'slug',
        'seo_title',
        'search_description',
    )

    content_panels = [
        FieldPanel('title'),
        StreamFieldPanel('body'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    def serve(self, request, *args, **kwargs):
        data = {
            'title': self.title,
            'slug': self.slug,
            'seo_title': self.seo_title,
            'search_description': self.search_description,
            'body': self.body,
        }

        return JsonResponse(data)

class EcosystemAllies(Page):
    page_description = models.TextField()

    @property
    def allies(self):
        allies = Ally.objects.all()
        ally_data = {}
        for ally in allies:
            ally_data[ally.slug] = {
                'title': ally.title,
                'subjects': ally.ally_subject_list(),
                'short_description': ally.short_description,
                'long_description': ally.long_description,
                'heading': ally.heading,
                'is_ap': ally.is_ap,
                'ally_bw_logo': ally.ally_bw_logo,
            }
        return ally_data

    api_fields = (
        'title',
        'page_description',
        'allies',
        'slug',
        'seo_title',
        'search_description',
    )

    content_panels = [
        FieldPanel('title', classname="full title"),
        FieldPanel('page_description'),
    ]

    promote_panels = [
        FieldPanel('slug'),
        FieldPanel('seo_title'),
        FieldPanel('search_description'),

    ]

    parent_page_types = ['pages.HomePage']
<|endoftext|>


<commit_before>"""
Some of these are clearly trivial, but included for completeness and
notational consistency.
"""

def AND(A, B):
    return int(A and B)

def AND3(A, B, C):
    return int(A and B and C)

def OR(A, B):
    return int(A or B)

def OR4(A, B, C, D):
    return int(A or B or C or D)

def NOT(A):
    return int(not A)

def NAND(A, B):
    return int(not (A and B))

def NOR(A, B):
    return int(not (A or B))

def XOR(A, B):
    return int((not A and B) or (A and not B))

def XNOR(A, B):
    return int((not A and not B) or (A and B))
<commit_msg>Defined 4-input AND<commit_after>"""
Some of these are clearly trivial, but included for completeness and
notational consistency.
"""

def AND(A, B):
    return int(A and B)

def AND3(A, B, C):
    return int(A and B and C)

def AND4(A, B, C, D):
    return int(A and B and C and D)

def OR(A, B):
    return int(A or B)

def OR4(A, B, C, D):
    return int(A or B or C or D)

def NOT(A):
    return int(not A)

def NAND(A, B):
    return int(not (A and B))

def NOR(A, B):
    return int(not (A or B))

def XOR(A, B):
    return int((not A and B) or (A and not B))

def XNOR(A, B):
    return int((not A and not B) or (A and B))
<|endoftext|>


<commit_before>

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app  # noqa

__version__ = '1.5.4'
__url__ = 'https://github.com/DefectDojo/django-DefectDojo'
__docs__ = 'http://defectdojo.readthedocs.io/'
__demo__ = 'http://defectdojo.pythonanywhere.com/'
<commit_msg>Fix dojo version (#2273)<commit_after>

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app  # noqa

__version__ = '1.7.0-dev'
__url__ = 'https://github.com/DefectDojo/django-DefectDojo'
__docs__ = 'http://defectdojo.readthedocs.io/'
<|endoftext|>