From ff9945037fc27e2053712feef2c4f613d9581ccd Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 15 Aug 2016 14:50:53 -0400 Subject: [PATCH 01/21] Initialize xmlsec once to prevent SAML auth from hanging. --- awx/sso/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/awx/sso/__init__.py b/awx/sso/__init__.py index e484e62be1..347aedfeee 100644 --- a/awx/sso/__init__.py +++ b/awx/sso/__init__.py @@ -1,2 +1,21 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. + +# Python +import threading + +# Monkeypatch xmlsec.initialize() to only run once (https://github.com/ansible/ansible-tower/issues/3241). +xmlsec_init_lock = threading.Lock() +xmlsec_initialized = False + +import dm.xmlsec.binding +original_xmlsec_initialize = dm.xmlsec.binding.initialize + +def xmlsec_initialize(*args, **kwargs): + global xmlsec_init_lock, xmlsec_initialized, original_xmlsec_initialize + with xmlsec_init_lock: + if not xmlsec_initialized: + original_xmlsec_initialize(*args, **kwargs) + xmlsec_initialized = True + +dm.xmlsec.binding.initialize = xmlsec_initialize From cef7f5a16597119d55958eea2b3dd82d7b27f6e0 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 18 Aug 2016 09:55:20 -0400 Subject: [PATCH 02/21] prevent non-superusers from adding orphan users --- awx/api/permissions.py | 9 ++++++++- awx/api/views.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index 6e1320e2d8..285441421d 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -19,7 +19,7 @@ from awx.main.utils import get_object_or_400 logger = logging.getLogger('awx.api.permissions') __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', - 'TaskPermission', 'ProjectUpdatePermission'] + 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission'] class ModelAccessPermission(permissions.BasePermission): ''' @@ -202,3 +202,10 @@ class ProjectUpdatePermission(ModelAccessPermission): def check_post_permissions(self, request, view, obj=None): project = get_object_or_400(view.model, pk=view.kwargs['pk']) return check_user_access(request.user, view.model, 'start', project) + + +class UserPermission(ModelAccessPermission): + def check_post_permissions(self, request, view, obj=None): + if request.user.is_superuser: + return True + raise PermissionDenied() diff --git a/awx/api/views.py b/awx/api/views.py index 0b20304892..a58b706856 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1152,6 +1152,7 @@ class UserList(ListCreateAPIView): model = User serializer_class = UserSerializer + permission_classes = (UserPermission,) def post(self, request, *args, **kwargs): ret = super(UserList, self).post( request, *args, **kwargs) From 5b00bb14ca726f4d861e24e8b4c6df050e312e5b Mon Sep 17 00:00:00 2001 From: jangsutsr Date: Thu, 18 Aug 2016 18:59:33 -0400 Subject: [PATCH 03/21] Modify job event save behavior --- awx/main/models/jobs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index bbf53b86ce..e19bd67d3d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1106,6 +1106,10 @@ class JobEvent(CreatedModifiedModel): self.failed = True if 'failed' not in update_fields: update_fields.append('failed') + else: + self.event = 'runner_on_skipped' + if 'changed' in res: + res['changed'] = False if isinstance(res, dict) and res.get('changed', False): self.changed = True if 'changed' not in update_fields: From a847495bea89c66fca279e99573aaf2b1cd2d430 Mon Sep 17 00:00:00 2001 From: James Laska Date: Wed, 17 Aug 2016 13:12:32 -0400 Subject: [PATCH 04/21] Update foreman inventory script Per request from Red Hat Satellite team, update to the latest foreman.py https://github.com/theforeman/foreman_ansible_inventory/blob/master/foreman_ansible_inventory.py --- awx/plugins/inventory/foreman.py | 106 ++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/awx/plugins/inventory/foreman.py b/awx/plugins/inventory/foreman.py index ce057690df..ddcb912fd5 100755 --- a/awx/plugins/inventory/foreman.py +++ b/awx/plugins/inventory/foreman.py @@ -1,8 +1,6 @@ -#!/usr/bin/python +#!/usr/bin/env python # vim: set fileencoding=utf-8 : # -# NOTE FOR TOWER: change foreman_ to sattelite_ for the group prefix -# # Copyright (C) 2016 Guido Günther # # This script is free software: you can redistribute it and/or modify @@ -41,6 +39,7 @@ class ForemanInventory(object): self.inventory = dict() # A list of groups and the hosts in that group self.cache = dict() # Details about hosts in the inventory self.params = dict() # Params of each host + self.facts = dict() # Facts of each host self.hostgroups = dict() # host groups # Read settings and parse CLI arguments @@ -55,6 +54,7 @@ class ForemanInventory(object): else: self.load_inventory_from_cache() self.load_params_from_cache() + self.load_facts_from_cache() self.load_cache_from_cache() data_to_print = "" @@ -69,6 +69,9 @@ class ForemanInventory(object): 'foreman': self.cache[hostname], 'foreman_params': self.params[hostname], } + if self.want_facts: + self.inventory['_meta']['hostvars'][hostname]['foreman_facts'] = self.facts[hostname] + data_to_print += self.json_format_dict(self.inventory, True) print(data_to_print) @@ -81,7 +84,8 @@ class ForemanInventory(object): current_time = time() if (mod_time + self.cache_max_age) > current_time: if (os.path.isfile(self.cache_path_inventory) and - os.path.isfile(self.cache_path_params)): + os.path.isfile(self.cache_path_params) and + os.path.isfile(self.cache_path_facts)): return True return False @@ -114,6 +118,16 @@ class ForemanInventory(object): self.group_patterns = eval(group_patterns) + try: + self.group_prefix = config.get('ansible', 'group_prefix') + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + self.group_prefix = "foreman_" + + try: + self.want_facts = config.getboolean('ansible', 'want_facts') + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + self.want_facts = True + # Cache related try: cache_path = os.path.expanduser(config.get('cache', 'path')) @@ -123,6 +137,7 @@ class ForemanInventory(object): self.cache_path_cache = cache_path + "/%s.cache" % script self.cache_path_inventory = cache_path + "/%s.index" % script self.cache_path_params = cache_path + "/%s.params" % script + self.cache_path_facts = cache_path + "/%s.facts" % script self.cache_max_age = config.getint('cache', 'max_age') def parse_cli_args(self): @@ -135,7 +150,7 @@ class ForemanInventory(object): help='Force refresh of cache by making API requests to foreman (default: False - use cache files)') self.args = parser.parse_args() - def _get_json(self, url): + def _get_json(self, url, ignore_errors=None): page = 1 results = [] while True: @@ -143,10 +158,14 @@ class ForemanInventory(object): auth=HTTPBasicAuth(self.foreman_user, self.foreman_pw), verify=self.foreman_ssl_verify, params={'page': page, 'per_page': 250}) + if ignore_errors and ret.status_code in ignore_errors: + break ret.raise_for_status() json = ret.json() if not json.has_key('results'): return json + if type(json['results']) == type({}): + return json['results'] results = results + json['results'] if len(results) >= json['total']: break @@ -162,38 +181,44 @@ class ForemanInventory(object): self.hostgroups[hid] = self._get_json(url) return self.hostgroups[hid] - def _get_params_by_id(self, hid): - url = "%s/api/v2/hosts/%s/parameters" % (self.foreman_url, hid) + def _get_all_params_by_id(self, hid): + url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid) + ret = self._get_json(url, [404]) + if ret == []: ret = {} + return ret.get('all_parameters', {}) + + def _get_facts_by_id(self, hid): + url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid) return self._get_json(url) def _resolve_params(self, host): """ - Resolve all host group params of the host using the top level - hostgroup and the ancestry. + Fetch host params and convert to dict """ - hostgroup_id = host['hostgroup_id'] - paramgroups = [] params = {} - if hostgroup_id: - hostgroup = self._get_hostgroup_by_id(hostgroup_id) - ancestry_path = hostgroup.get('ancestry', '') - ancestry = ancestry_path.split('/') if ancestry_path is not None else [] - - # Append top level hostgroup last to overwrite lower levels - # values - ancestry.append(hostgroup_id) - paramgroups = [self._get_hostgroup_by_id(hostgroup_id)['parameters'] - for hostgroup_id in ancestry] - - paramgroups += [self._get_params_by_id(host['id'])] - for paramgroup in paramgroups: - for param in paramgroup: - name = param['name'] - params[name] = param['value'] + for param in self._get_all_params_by_id(host['id']): + name = param['name'] + params[name] = param['value'] return params + def _get_facts(self, host): + """ + Fetch all host facts of the host + """ + if not self.want_facts: + return {} + + ret = self._get_facts_by_id(host['id']) + if len(ret.values()) == 0: + facts = {} + elif len(ret.values()) == 1: + facts = ret.values()[0] + else: + raise ValueError("More than one set of facts returned for '%s'" % host) + return facts + def update_cache(self): """Make calls to foreman and save the output in a cache""" @@ -203,11 +228,17 @@ class ForemanInventory(object): for host in self._get_hosts(): dns_name = host['name'] - # Create ansible groups for hostgroup, location and organization - for group in ['hostgroup', 'location', 'organization']: + # Create ansible groups for hostgroup, environment, location and organization + for group in ['hostgroup', 'environment', 'location', 'organization']: val = host.get('%s_name' % group) if val: - safe_key = self.to_safe('satellite_%s_%s' % (group, val.lower())) + safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower())) + self.push(self.inventory, safe_key, dns_name) + + for group in ['lifecycle_environment', 'content_view']: + val = host.get('content_facet_attributes', {}).get('%s_name' % group) + if val: + safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower())) self.push(self.inventory, safe_key, dns_name) params = self._resolve_params(host) @@ -231,11 +262,13 @@ class ForemanInventory(object): self.cache[dns_name] = host self.params[dns_name] = params + self.facts[dns_name] = self._get_facts(host) self.push(self.inventory, 'all', dns_name) self.write_to_cache(self.cache, self.cache_path_cache) self.write_to_cache(self.inventory, self.cache_path_inventory) self.write_to_cache(self.params, self.cache_path_params) + self.write_to_cache(self.facts, self.cache_path_facts) def get_host_info(self): """ Get variables about a specific host """ @@ -274,6 +307,14 @@ class ForemanInventory(object): json_params = cache.read() self.params = json.loads(json_params) + def load_facts_from_cache(self): + """ Reads the index from the cache file sets self.index """ + if not self.want_facts: + return + cache = open(self.cache_path_facts, 'r') + json_facts = cache.read() + self.facts = json.loads(json_facts) + def load_cache_from_cache(self): """ Reads the cache from the cache file sets self.cache """ @@ -301,4 +342,7 @@ class ForemanInventory(object): else: return json.dumps(data) -ForemanInventory() +if __name__ == '__main__': + ForemanInventory() + + From 188f81ef3d54d14f055593bd91d165676ee4f0cf Mon Sep 17 00:00:00 2001 From: James Laska Date: Fri, 19 Aug 2016 09:30:15 -0400 Subject: [PATCH 05/21] Update cloudforms dynamic inventory Pulling the latest from https://github.com/ansible/ansible/blob/devel/contrib/inventory/cloudforms.py Related #3168 --- awx/main/tasks.py | 7 +- awx/plugins/inventory/cloudforms.py | 504 +++++++++++++++++++++++----- 2 files changed, 417 insertions(+), 94 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 877ed4b2d2..c99f043e1a 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1319,9 +1319,14 @@ class RunInventoryUpdate(BaseTask): credential = inventory_update.credential if credential: - cp.set(section, 'hostname', credential.host) + cp.set(section, 'url', credential.host) cp.set(section, 'username', credential.username) cp.set(section, 'password', decrypt_field(credential, 'password')) + cp.set(section, 'ssl_verify', "false") + + section = 'cache' + cp.add_section(section) + cp.set(section, 'max_age', "0") elif inventory_update.source == 'azure_rm': section = 'azure' diff --git a/awx/plugins/inventory/cloudforms.py b/awx/plugins/inventory/cloudforms.py index 8d9854974f..65d95853d5 100755 --- a/awx/plugins/inventory/cloudforms.py +++ b/awx/plugins/inventory/cloudforms.py @@ -1,144 +1,462 @@ #!/usr/bin/python +# vim: set fileencoding=utf-8 : +# +# Copyright (C) 2016 Guido Günther +# +# This script is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with it. If not, see . +# +# This is loosely based on the foreman inventory script +# -- Josh Preston +# -''' -CloudForms external inventory script -================================================== -Generates inventory that Ansible can understand by making API request to CloudForms. -Modeled after https://raw.githubusercontent.com/ansible/ansible/stable-1.9/plugins/inventory/ec2.py -jlabocki redhat.com or @jameslabocki on twitter -''' - -import os +from __future__ import print_function import argparse import ConfigParser +import os +import re +from time import time import requests -import json +from requests.auth import HTTPBasicAuth +import warnings -# This disables warnings and is not a good idea, but hey, this is a demo -# http://urllib3.readthedocs.org/en/latest/security.html#disabling-warnings -requests.packages.urllib3.disable_warnings() +try: + import json +except ImportError: + import simplejson as json class CloudFormsInventory(object): - - def _empty_inventory(self): - return {"_meta": {"hostvars": {}}} - def __init__(self): - ''' Main execution path ''' + """ + Main execution path + """ + self.inventory = dict() # A list of groups and the hosts in that group + self.hosts = dict() # Details about hosts in the inventory - # Inventory grouped by instance IDs, tags, security groups, regions, - # and availability zones - self.inventory = self._empty_inventory() - - # Index of hostname (address) to instance ID - self.index = {} - - # Read CLI arguments - self.read_settings() + # Parse CLI arguments self.parse_cli_args() - # Get Hosts - if self.args.list: - self.get_hosts() + # Read settings + self.read_settings() - # This doesn't exist yet and needs to be added + # Cache + if self.args.refresh_cache or not self.is_cache_valid(): + self.update_cache() + else: + self.load_inventory_from_cache() + self.load_hosts_from_cache() + + data_to_print = "" + + # Data to print if self.args.host: - data2 = {} - print json.dumps(data2, indent=2) + if self.args.debug: + print("Fetching host [%s]" % self.args.host) + data_to_print += self.get_host_info(self.args.host) + else: + self.inventory['_meta'] = {'hostvars': {}} + for hostname in self.hosts: + self.inventory['_meta']['hostvars'][hostname] = { + 'cloudforms': self.hosts[hostname], + } + # include the ansible_ssh_host in the top level + if 'ansible_ssh_host' in self.hosts[hostname]: + self.inventory['_meta']['hostvars'][hostname]['ansible_ssh_host'] = self.hosts[hostname]['ansible_ssh_host'] - def parse_cli_args(self): - ''' Command line argument processing ''' + data_to_print += self.json_format_dict(self.inventory, self.args.pretty) - parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms') - parser.add_argument('--list', action='store_true', default=False, - help='List instances (default: False)') - parser.add_argument('--host', action='store', - help='Get all the variables about a specific instance') - self.args = parser.parse_args() + print(data_to_print) + + def is_cache_valid(self): + """ + Determines if the cache files have expired, or if it is still valid + """ + if self.args.debug: + print("Determining if cache [%s] is still valid (< %s seconds old)" % (self.cache_path_hosts, self.cache_max_age)) + + if os.path.isfile(self.cache_path_hosts): + mod_time = os.path.getmtime(self.cache_path_hosts) + current_time = time() + if (mod_time + self.cache_max_age) > current_time: + if os.path.isfile(self.cache_path_inventory): + if self.args.debug: + print("Cache is still valid!") + return True + + if self.args.debug: + print("Cache is stale or does not exist.") + + return False def read_settings(self): - ''' Reads the settings from the cloudforms.ini file ''' - + """ + Reads the settings from the cloudforms.ini file + """ config = ConfigParser.SafeConfigParser() config_paths = [ - os.path.join(os.path.dirname(os.path.realpath(__file__)), 'cloudforms.ini'), - "/opt/rh/cloudforms.ini", + os.path.dirname(os.path.realpath(__file__)) + '/cloudforms.ini', + "/etc/ansible/cloudforms.ini", ] env_value = os.environ.get('CLOUDFORMS_INI_PATH') if env_value is not None: config_paths.append(os.path.expanduser(os.path.expandvars(env_value))) + if self.args.debug: + for config_path in config_paths: + print("Reading from configuration file [%s]" % config_path) + config.read(config_paths) - # Version - if config.has_option('cloudforms', 'version'): - self.cloudforms_version = config.get('cloudforms', 'version') + # CloudForms API related + if config.has_option('cloudforms', 'url'): + self.cloudforms_url = config.get('cloudforms', 'url') else: - self.cloudforms_version = "none" + self.cloudforms_url = None - # CloudForms Endpoint - if config.has_option('cloudforms', 'hostname'): - self.cloudforms_hostname = config.get('cloudforms', 'hostname') - else: - self.cloudforms_hostname = None + if not self.cloudforms_url: + warnings.warn("No url specified, expected something like 'https://cfme.example.com'") - # CloudForms Username if config.has_option('cloudforms', 'username'): self.cloudforms_username = config.get('cloudforms', 'username') else: - self.cloudforms_username = "none" + self.cloudforms_username = None + + if not self.cloudforms_username: + warnings.warn("No username specified, you need to specify a CloudForms username.") - # CloudForms Password if config.has_option('cloudforms', 'password'): - self.cloudforms_password = config.get('cloudforms', 'password') + self.cloudforms_pw = config.get('cloudforms', 'password') else: - self.cloudforms_password = "none" + self.cloudforms_pw = None - def get_hosts(self): - ''' Gets host from CloudForms ''' - r = requests.get("https://{0}/api/vms?expand=resources&attributes=all".format(self.cloudforms_hostname), - auth=(self.cloudforms_username, self.cloudforms_password), verify=False) - obj = r.json() + if not self.cloudforms_pw: + warnings.warn("No password specified, you need to specify a password for the CloudForms user.") - # Create groups+hosts based on host data - for resource in obj.get('resources', []): + if config.has_option('cloudforms', 'ssl_verify'): + self.cloudforms_ssl_verify = config.getboolean('cloudforms', 'ssl_verify') + else: + self.cloudforms_ssl_verify = True - # Maintain backwards compat by creating `Dynamic_CloudForms` group - if 'Dynamic_CloudForms' not in self.inventory: - self.inventory['Dynamic_CloudForms'] = [] - self.inventory['Dynamic_CloudForms'].append(resource['name']) + if config.has_option('cloudforms', 'version'): + self.cloudforms_version = config.get('cloudforms', 'version') + else: + self.cloudforms_version = None - # Add host to desired groups - for key in ('vendor', 'type', 'location'): - if key in resource: - # Create top-level group - if key not in self.inventory: - self.inventory[key] = dict(children=[], vars={}, hosts=[]) - # if resource['name'] not in self.inventory[key]['hosts']: - # self.inventory[key]['hosts'].append(resource['name']) + if config.has_option('cloudforms', 'limit'): + self.cloudforms_limit = config.getint('cloudforms', 'limit') + else: + self.cloudforms_limit = 100 - # Create sub-group - if resource[key] not in self.inventory: - self.inventory[resource[key]] = dict(children=[], vars={}, hosts=[]) - # self.inventory[resource[key]]['hosts'].append(resource['name']) + if config.has_option('cloudforms', 'purge_actions'): + self.cloudforms_purge_actions = config.getboolean('cloudforms', 'purge_actions') + else: + self.cloudforms_purge_actions = True - # Add sub-group, as a child of top-level - if resource[key] not in self.inventory[key]['children']: - self.inventory[key]['children'].append(resource[key]) + if config.has_option('cloudforms', 'clean_group_keys'): + self.cloudforms_clean_group_keys = config.getboolean('cloudforms', 'clean_group_keys') + else: + self.cloudforms_clean_group_keys = True + if config.has_option('cloudforms', 'nest_tags'): + self.cloudforms_nest_tags = config.getboolean('cloudforms', 'nest_tags') + else: + self.cloudforms_nest_tags = False + + # Ansible related + try: + group_patterns = config.get('ansible', 'group_patterns') + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + group_patterns = "[]" + + self.group_patterns = eval(group_patterns) + + # Cache related + try: + cache_path = os.path.expanduser(config.get('cache', 'path')) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + cache_path = '.' + (script, ext) = os.path.splitext(os.path.basename(__file__)) + self.cache_path_hosts = cache_path + "/%s.hosts" % script + self.cache_path_inventory = cache_path + "/%s.inventory" % script + self.cache_max_age = config.getint('cache', 'max_age') + + if self.args.debug: + print("CloudForms settings:") + print("cloudforms_url = %s" % self.cloudforms_url) + print("cloudforms_username = %s" % self.cloudforms_username) + print("cloudforms_pw = %s" % self.cloudforms_pw) + print("cloudforms_ssl_verify = %s" % self.cloudforms_ssl_verify) + print("cloudforms_version = %s" % self.cloudforms_version) + print("cloudforms_limit = %s" % self.cloudforms_limit) + print("cloudforms_purge_actions = %s" % self.cloudforms_purge_actions) + print("Cache settings:") + print("cache_max_age = %s" % self.cache_max_age) + print("cache_path_hosts = %s" % self.cache_path_hosts) + print("cache_path_inventory = %s" % self.cache_path_inventory) + + def parse_cli_args(self): + """ + Command line argument processing + """ + parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms managed VMs') + parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)') + parser.add_argument('--host', action='store', help='Get all the variables about a specific instance') + parser.add_argument('--pretty', action='store_true', default=False, help='Pretty print JSON output (default: False)') + parser.add_argument('--refresh-cache', action='store_true', default=False, + help='Force refresh of cache by making API requests to CloudForms (default: False - use cache files)') + parser.add_argument('--debug', action='store_true', default=False, help='Show debug output while running (default: False)') + self.args = parser.parse_args() + + def _get_json(self, url): + """ + Make a request and return the JSON + """ + results = [] + + ret = requests.get(url, + auth=HTTPBasicAuth(self.cloudforms_username, self.cloudforms_pw), + verify=self.cloudforms_ssl_verify) + + ret.raise_for_status() + + try: + results = json.loads(ret.text) + except ValueError: + warnings.warn("Unexpected response from {0} ({1}): {2}".format(self.cloudforms_url, ret.status_code, ret.reason)) + results = {} + + if self.args.debug: + print("=======================================================================") + print("=======================================================================") + print("=======================================================================") + print(ret.text) + print("=======================================================================") + print("=======================================================================") + print("=======================================================================") + + return results + + def _get_hosts(self): + """ + Get all hosts by paging through the results + """ + limit = self.cloudforms_limit + + page = 0 + last_page = False + + results = [] + + while not last_page: + offset = page * limit + ret = self._get_json("%s/api/vms?offset=%s&limit=%s&expand=resources,tags,hosts,&attributes=ipaddresses" % (self.cloudforms_url, offset, limit)) + results += ret['resources'] + if ret['subcount'] < limit: + last_page = True + page += 1 + + return results + + def update_cache(self): + """ + Make calls to cloudforms and save the output in a cache + """ + self.groups = dict() + self.hosts = dict() + + if self.args.debug: + print("Updating cache...") + + for host in self._get_hosts(): + # Ignore VMs that are not powered on + if host['power_state'] != 'on': + if self.args.debug: + print("Skipping %s because power_state = %s" % (host['name'], host['power_state'])) + continue + + # purge actions + if self.cloudforms_purge_actions and 'actions' in host: + del host['actions'] + + # Create ansible groups for tags + if 'tags' in host: + + # Create top-level group + if 'tags' not in self.inventory: + self.inventory['tags'] = dict(children=[], vars={}, hosts=[]) + + if not self.cloudforms_nest_tags: + # don't expand tags, just use them in a safe way + for group in host['tags']: + # Add sub-group, as a child of top-level + safe_key = self.to_safe(group['name']) + if safe_key: + if self.args.debug: + print("Adding sub-group '%s' to parent 'tags'" % safe_key) + + if safe_key not in self.inventory['tags']['children']: + self.push(self.inventory['tags'], 'children', safe_key) + + self.push(self.inventory, safe_key, host['name']) + + if self.args.debug: + print("Found tag [%s] for host which will be mapped to [%s]" % (group['name'], safe_key)) + else: + # expand the tags into nested groups / sub-groups + # Create nested groups for tags + safe_parent_tag_name = 'tags' + for tag in host['tags']: + tag_hierarchy = tag['name'][1:].split('/') + + if self.args.debug: + print("Working on list %s" % tag_hierarchy) + + for tag_name in tag_hierarchy: + if self.args.debug: + print("Working on tag_name = %s" % tag_name) + + safe_tag_name = self.to_safe(tag_name) + if self.args.debug: + print("Using sanitized name %s" % safe_tag_name) + + # Create sub-group + if safe_tag_name not in self.inventory: + self.inventory[safe_tag_name] = dict(children=[], vars={}, hosts=[]) + + # Add sub-group, as a child of top-level + if safe_parent_tag_name: + if self.args.debug: + print("Adding sub-group '%s' to parent '%s'" % (safe_tag_name, safe_parent_tag_name)) + + if safe_tag_name not in self.inventory[safe_parent_tag_name]['children']: + self.push(self.inventory[safe_parent_tag_name], 'children', safe_tag_name) + + # Make sure the next one uses this one as it's parent + safe_parent_tag_name = safe_tag_name + + # Add the host to the last tag + self.push(self.inventory[safe_parent_tag_name], 'hosts', host['name']) + + # Set ansible_ssh_host to the first available ip address + if 'ipaddresses' in host and host['ipaddresses'] and isinstance(host['ipaddresses'], list): + host['ansible_ssh_host'] = host['ipaddresses'][0] + + # Create additional groups + for key in ('location', 'type', 'vendor'): + safe_key = self.to_safe(host[key]) + + # Create top-level group + if key not in self.inventory: + self.inventory[key] = dict(children=[], vars={}, hosts=[]) + + # Create sub-group + if safe_key not in self.inventory: + self.inventory[safe_key] = dict(children=[], vars={}, hosts=[]) + + # Add sub-group, as a child of top-level + if safe_key not in self.inventory[key]['children']: + self.push(self.inventory[key], 'children', safe_key) + + if key in host: # Add host to sub-group - if resource['name'] not in self.inventory[resource[key]]: - self.inventory[resource[key]]['hosts'].append(resource['name']) + self.push(self.inventory[safe_key], 'hosts', host['name']) - # Delete 'actions' key - del resource['actions'] + self.hosts[host['name']] = host + self.push(self.inventory, 'all', host['name']) - # Add _meta hostvars - self.inventory['_meta']['hostvars'][resource['name']] = resource + if self.args.debug: + print("Saving cached data") - print json.dumps(self.inventory, indent=2) + self.write_to_cache(self.hosts, self.cache_path_hosts) + self.write_to_cache(self.inventory, self.cache_path_inventory) + + def get_host_info(self, host): + """ + Get variables about a specific host + """ + if not self.hosts or len(self.hosts) == 0: + # Need to load cache from cache + self.load_hosts_from_cache() + + if host not in self.hosts: + if self.args.debug: + print("[%s] not found in cache." % host) + + # try updating the cache + self.update_cache() + + if host not in self.hosts: + if self.args.debug: + print("[%s] does not exist after cache update." % host) + # host might not exist anymore + return self.json_format_dict({}, self.args.pretty) + + return self.json_format_dict(self.hosts[host], self.args.pretty) + + def push(self, d, k, v): + """ + Safely puts a new entry onto an array. + """ + if k in d: + d[k].append(v) + else: + d[k] = [v] + + def load_inventory_from_cache(self): + """ + Reads the inventory from the cache file sets self.inventory + """ + cache = open(self.cache_path_inventory, 'r') + json_inventory = cache.read() + self.inventory = json.loads(json_inventory) + + def load_hosts_from_cache(self): + """ + Reads the cache from the cache file sets self.hosts + """ + cache = open(self.cache_path_hosts, 'r') + json_cache = cache.read() + self.hosts = json.loads(json_cache) + + def write_to_cache(self, data, filename): + """ + Writes data in JSON format to a file + """ + json_data = self.json_format_dict(data, True) + cache = open(filename, 'w') + cache.write(json_data) + cache.close() + + def to_safe(self, word): + """ + Converts 'bad' characters in a string to underscores so they can be used as Ansible groups + """ + if self.cloudforms_clean_group_keys: + regex = "[^A-Za-z0-9\_]" + return re.sub(regex, "_", word.replace(" ", "")) + else: + return word + + def json_format_dict(self, data, pretty=False): + """ + Converts a dict to a JSON object and dumps it as a formatted string + """ + if pretty: + return json.dumps(data, sort_keys=True, indent=2) + else: + return json.dumps(data) -# Run the script CloudFormsInventory() + From 08c4c9647b083f5a661a57caf78dc1169d8150cf Mon Sep 17 00:00:00 2001 From: James Laska Date: Fri, 19 Aug 2016 15:33:18 -0400 Subject: [PATCH 06/21] Resolve KeyError by coercing instance_id to a str Related #3300 --- awx/main/management/commands/inventory_import.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 21031f23cc..4ae521cd5c 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -22,6 +22,7 @@ import yaml from django.conf import settings from django.core.management.base import NoArgsCommand, CommandError from django.db import connection, transaction +from django.utils.encoding import smart_text # AWX from awx.main.models import * # noqa @@ -606,7 +607,7 @@ class Command(NoArgsCommand): break instance_id = from_dict.get(key, default) from_dict = instance_id - return instance_id + return smart_text(instance_id) def _get_enabled(self, from_dict, default=None): ''' From f5b4c631731989f47109397b643f1595b5198e54 Mon Sep 17 00:00:00 2001 From: Graham Mainwaring Date: Fri, 19 Aug 2016 16:28:09 -0400 Subject: [PATCH 07/21] Clean venv on 'make clean' --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b7e3fd9d21..fe806fcd57 100644 --- a/Makefile +++ b/Makefile @@ -172,7 +172,7 @@ endif .DEFAULT_GOAL := build -.PHONY: clean clean-tmp rebase push requirements requirements_dev \ +.PHONY: clean clean-tmp clean-venv rebase push requirements requirements_dev \ requirements_jenkins \ develop refresh adduser migrate dbchange dbshell runserver celeryd \ receiver test test_unit test_coverage coverage_html test_jenkins dev_build \ @@ -229,8 +229,11 @@ clean-build-test: clean-tmp: rm -rf tmp/ +clean-venv: + rm -rf venv/ + # Remove temporary build files, compiled Python files. -clean: clean-rpm clean-deb clean-grunt clean-ui clean-static clean-build-test clean-tar clean-packer clean-bundle +clean: clean-rpm clean-deb clean-grunt clean-ui clean-static clean-build-test clean-tar clean-packer clean-bundle clean-venv rm -rf awx/lib/site-packages rm -rf awx/lib/.deps_built rm -rf dist/* From 706e98c392a723c485123e931755dd59338ddc40 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 19 Aug 2016 18:54:56 -0400 Subject: [PATCH 08/21] fixing old tests for new user creation permissions --- awx/main/tests/old/users.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/awx/main/tests/old/users.py b/awx/main/tests/old/users.py index de364ff161..6fc7726512 100644 --- a/awx/main/tests/old/users.py +++ b/awx/main/tests/old/users.py @@ -192,8 +192,12 @@ class UsersTest(BaseTest): self.post(url, expect=403, data=new_user, auth=self.get_other_credentials()) self.post(url, expect=201, data=new_user, auth=self.get_super_credentials()) self.post(url, expect=400, data=new_user, auth=self.get_super_credentials()) - self.post(url, expect=201, data=new_user2, auth=self.get_normal_credentials()) - self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials()) + # org admin cannot create orphaned users + self.post(url, expect=403, data=new_user2, auth=self.get_normal_credentials()) + # org admin can create org users + org_url = reverse('api:organization_users_list', args=(self.organizations[0].pk,)) + self.post(org_url, expect=201, data=new_user2, auth=self.get_normal_credentials()) + self.post(org_url, expect=400, data=new_user2, auth=self.get_normal_credentials()) # Normal user cannot add users after his org is marked inactive. self.organizations[0].delete() new_user3 = dict(username='blippy3') @@ -367,23 +371,20 @@ class UsersTest(BaseTest): url = reverse('api:user_list') data = dict(username='username', password='password') data2 = dict(username='username2', password='password2') - data = self.post(url, expect=201, data=data, auth=self.get_normal_credentials()) + # but a regular user cannot create users + self.post(url, expect=403, data=data2, auth=self.get_other_credentials()) + # org admins cannot create orphaned users + self.post(url, expect=403, data=data2, auth=self.get_normal_credentials()) + + # a super user can create new users + self.post(url, expect=201, data=data, auth=self.get_super_credentials()) # verify that the login works... self.get(url, expect=200, auth=('username', 'password')) - # but a regular user cannot - data = self.post(url, expect=403, data=data2, auth=self.get_other_credentials()) - - # a super user can also create new users - data = self.post(url, expect=201, data=data2, auth=self.get_super_credentials()) - - # verify that the login works - self.get(url, expect=200, auth=('username2', 'password2')) - # verify that if you post a user with a pk, you do not alter that user's password info mod = dict(id=self.super_django_user.pk, username='change', password='change') - data = self.post(url, expect=201, data=mod, auth=self.get_super_credentials()) + self.post(url, expect=201, data=mod, auth=self.get_super_credentials()) orig = User.objects.get(pk=self.super_django_user.pk) self.assertTrue(orig.username != 'change') From 16fce595128698e2c5e47474a998f74e6899ee8a Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 22 Aug 2016 13:34:08 -0400 Subject: [PATCH 09/21] Revert "Modify job event save behavior" --- awx/main/models/jobs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index e19bd67d3d..bbf53b86ce 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1106,10 +1106,6 @@ class JobEvent(CreatedModifiedModel): self.failed = True if 'failed' not in update_fields: update_fields.append('failed') - else: - self.event = 'runner_on_skipped' - if 'changed' in res: - res['changed'] = False if isinstance(res, dict) and res.get('changed', False): self.changed = True if 'changed' not in update_fields: From 6cf567e4fd45f41abe0d1e9473f067aff3745f1b Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 22 Aug 2016 13:34:52 -0400 Subject: [PATCH 10/21] Revert "Prevent ignored task from being displayed as failing." --- awx/api/views.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 0b20304892..201d40fe5d 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1,3 +1,4 @@ + # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. @@ -3005,7 +3006,7 @@ class JobJobTasksList(BaseJobEventsList): # need stats on grandchildren, sorted by child. queryset = (JobEvent.objects.filter(parent__parent=parent_task, parent__event__in=STARTING_EVENTS) - .values('parent__id', 'event', 'changed', 'failed') + .values('parent__id', 'event', 'changed') .annotate(num=Count('event')) .order_by('parent__id')) @@ -3066,13 +3067,10 @@ class JobJobTasksList(BaseJobEventsList): # make appropriate changes to the task data. for child_data in data.get(task_start_event.id, []): if child_data['event'] == 'runner_on_failed': + task_data['failed'] = True task_data['host_count'] += child_data['num'] task_data['reported_hosts'] += child_data['num'] - if child_data['failed']: - task_data['failed'] = True - task_data['failed_count'] += child_data['num'] - else: - task_data['skipped_count'] += child_data['num'] + task_data['failed_count'] += child_data['num'] elif child_data['event'] == 'runner_on_ok': task_data['host_count'] += child_data['num'] task_data['reported_hosts'] += child_data['num'] From 2fb386b23c98689c2ef96dd5fc885af8264c0c64 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 23 Aug 2016 09:55:00 -0400 Subject: [PATCH 11/21] allow users to edit their first and last name --- awx/api/views.py | 2 +- awx/main/tests/old/users.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 201d40fe5d..a8abb6c8d3 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1318,7 +1318,7 @@ class UserDetail(RetrieveUpdateDestroyAPIView): can_admin = request.user.can_access(User, 'admin', obj, request.data) su_only_edit_fields = ('is_superuser', 'is_system_auditor') - admin_only_edit_fields = ('last_name', 'first_name', 'username', 'is_active') + admin_only_edit_fields = ('username', 'is_active') fields_to_check = () if not request.user.is_superuser: diff --git a/awx/main/tests/old/users.py b/awx/main/tests/old/users.py index de364ff161..c581f8d592 100644 --- a/awx/main/tests/old/users.py +++ b/awx/main/tests/old/users.py @@ -325,9 +325,9 @@ class UsersTest(BaseTest): detail_url = reverse('api:user_detail', args=(self.other_django_user.pk,)) data = self.get(detail_url, expect=200, auth=self.get_other_credentials()) - # can't change first_name, last_name, etc + # can change first_name, last_name, etc data['last_name'] = "NewLastName" - self.put(detail_url, data, expect=403, auth=self.get_other_credentials()) + self.put(detail_url, data, expect=200, auth=self.get_other_credentials()) # can't change username data['username'] = 'newUsername' From 034f266b07d08ed2bf32eedd1418c50c07844597 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 23 Aug 2016 13:48:10 -0400 Subject: [PATCH 12/21] fix ng-toast rel, resolves #3197 (#3316) --- awx/ui/templates/ui/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 74e21e7be4..b4311edd26 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -15,7 +15,7 @@ - + From bb3e370e5645ed9c8de05b5d7e7236500101ade4 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 23 Aug 2016 14:06:00 -0400 Subject: [PATCH 13/21] Update team admin credential migration test to current state-of-knowledge --- awx/main/tests/functional/test_rbac_credential.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 8cac236fee..72ba6397ae 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -56,11 +56,12 @@ def test_credential_migration_team_member(credential, team, user, permissions): # Admin permissions post migration assert u in credential.use_role + assert u not in credential.admin_role @pytest.mark.django_db def test_credential_migration_team_admin(credential, team, user, permissions): u = user('user', False) - team.member_role.members.add(u) + team.admin_role.members.add(u) credential.deprecated_team = team credential.save() @@ -68,7 +69,7 @@ def test_credential_migration_team_admin(credential, team, user, permissions): # Usage permissions post migration rbac.migrate_credential(apps, None) - assert u in credential.use_role + assert u in credential.admin_role def test_credential_access_superuser(): u = User(username='admin', is_superuser=True) From 23024c8fadef1d1bea9712f652b2bebd9a0bdf75 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 24 Aug 2016 10:54:35 -0400 Subject: [PATCH 14/21] Make sure org admins can see credential after migration, comment updates on related tests add clause in test to verify automatic setting of org of new team credential --- .../tests/functional/api/test_credential.py | 4 +++- .../tests/functional/test_rbac_credential.py | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index 3c79e62e33..f1e7a2b1dd 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -71,7 +71,6 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob def test_create_team_credential(post, get, team, organization, org_admin, team_member): response = post(reverse('api:credential_list'), { 'team': team.id, - 'organization': organization.id, 'name': 'Some name', 'username': 'someusername' }, org_admin) @@ -81,6 +80,9 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m assert response.status_code == 200 assert response.data['count'] == 1 + # Assure that credential's organization is implictly set to team's org + assert response.data['results'][0]['summary_fields']['organization']['id'] == team.organization.id + @pytest.mark.django_db def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member): response = post(reverse('api:team_credentials_list', args=(team.pk,)), { diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 72ba6397ae..29c50f73e7 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -54,7 +54,7 @@ def test_credential_migration_team_member(credential, team, user, permissions): rbac.migrate_credential(apps, None) - # Admin permissions post migration + # User permissions post migration assert u in credential.use_role assert u not in credential.admin_role @@ -67,10 +67,27 @@ def test_credential_migration_team_admin(credential, team, user, permissions): assert u not in credential.use_role - # Usage permissions post migration + # Admin permissions post migration rbac.migrate_credential(apps, None) assert u in credential.admin_role +@pytest.mark.django_db +def test_credential_migration_org_auditor(credential, team, org_auditor): + # Team's organization is the org_auditor's org + credential.deprecated_team = team + credential.save() + + # No permissions pre-migration (this happens automatically so we patch this) + team.admin_role.children.remove(credential.admin_role) + team.member_role.children.remove(credential.use_role) + assert org_auditor not in credential.read_role + + rbac.migrate_credential(apps, None) + + # Read permissions post migration + assert org_auditor in credential.use_role + assert org_auditor in credential.read_role + def test_credential_access_superuser(): u = User(username='admin', is_superuser=True) access = CredentialAccess(u) From 0baeafa1f1f4540e04fedb55b52edf9a5162cba5 Mon Sep 17 00:00:00 2001 From: jlmitch5 Date: Wed, 24 Aug 2016 11:39:16 -0400 Subject: [PATCH 15/21] fix date locale angular scheduler --- .../lib/angular-scheduler.js | 1362 ++++++++--------- 1 file changed, 674 insertions(+), 688 deletions(-) diff --git a/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js b/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js index dfe3b7f8f8..b3d9d015c3 100644 --- a/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js +++ b/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js @@ -1,5 +1,5 @@ /*************************************************************************** - * angular-scheruler.js + * angular-scheduler.js * * Copyright (c) 2014 Ansible, Inc. * @@ -13,16 +13,32 @@ /* global RRule */ -'use strict'; - - -angular.module('underscore',[]) - .factory('_', [ function() { - return window._; - }]); - - -angular.module('AngularScheduler', ['underscore']) +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['lodash', 'angular', 'jquery', 'jquery-ui', 'moment'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require('lodash'), require('angular'), require('jquery'), require('jquery-ui'), require('moment')); + } else { + // Browser globals (root is window) + root.returnExports = factory(root._, root.angular, root.$, root.$.ui, root.moment); + } +}(this, function(_, angular, $, ui, moment) { + return angular.module('AngularScheduler', ['moment']) + .filter('schedulerDate', function() { + return function(input) { + var date; + if(input === null){ + return ""; + }else { + date = moment(input); + return date.format('l') + input.slice(input.indexOf(" ")); + } + }; + }) .constant('AngularScheduler.partials', '/lib/') .constant('AngularScheduler.useTimezone', false) @@ -30,189 +46,181 @@ angular.module('AngularScheduler', ['underscore']) // Initialize supporting scope variables and functions. Returns a scheduler object with getString(), // setString() and inject() methods. - .factory('SchedulerInit', ['$log', '$filter', '$timezones', 'LoadLookupValues', 'SetDefaults', 'CreateObject', '_', - 'AngularScheduler.useTimezone', 'AngularScheduler.showUTCField', 'InRange', - function($log, $filter, $timezones, LoadLookupValues, SetDefaults, CreateObject, _, useTimezone, showUTCField, InRange) { - return function(params) { + .factory('SchedulerInit', ['$log', '$filter', '$timezones', 'LoadLookupValues', 'SetDefaults', 'CreateObject', + 'AngularScheduler.useTimezone', 'AngularScheduler.showUTCField', 'InRange', + function($log, $filter, $timezones, LoadLookupValues, SetDefaults, CreateObject, useTimezone, showUTCField, InRange) { + return function(params) { - var scope = params.scope, - requireFutureStartTime = params.requireFutureStartTime || false; + var scope = params.scope, + requireFutureStartTime = params.requireFutureStartTime || false; - scope.schedulerShowTimeZone = useTimezone; - scope.schedulerShowUTCStartTime = showUTCField; + scope.schedulerShowTimeZone = useTimezone; + scope.schedulerShowUTCStartTime = showUTCField; - scope.setDefaults = function() { - if (useTimezone) { - scope.current_timezone = $timezones.getLocal(); - if ($.isEmptyObject(scope.current_timezone) || !scope.current_timezone.name) { - $log.error('Failed to find local timezone. Defaulting to America/New_York.'); - scope.current_timezone = { name: 'America/New_York' }; - } - // Set the to the browser's local timezone + scope.schedulerTimeZone = _.find(scope.timeZones, function(x) { + return x.name === scope.current_timezone.name; + }); + } + LoadLookupValues(scope); + SetDefaults(scope); + scope.scheduleTimeChange(); + scope.scheduleRepeatChange(); + }; + + scope.scheduleTimeChange = function(callback) { + if (scope.schedulerStartDt === "" || scope.schedulerStartDt === null || scope.schedulerStartDt === undefined) { + scope.startDateError("Provide a valid start date and time"); + scope.schedulerUTCTime = ''; + } else if (!(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2))) { + scope.scheduler_startTime_error = true; + } else { + if (useTimezone) { + scope.resetStartDate(); + try { + var dateStr = scope.schedulerStartDt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, function(match, p1, p2, p3) { return p3 + '-' + p1 + '-' + p2; }); - dateStr += 'T' + $filter('schZeroPad')(scope.schedulerStartHour, 2) + ':' + $filter('schZeroPad')(scope.schedulerStartMinute, 2) + ':' + - $filter('schZeroPad')(scope.schedulerStartSecond, 2) + '.000Z'; - scope.schedulerUTCTime = $filter('schDateStrFix')($timezones.toUTC(dateStr, scope.schedulerTimeZone.name).toISOString()); - scope.scheduler_form_schedulerStartDt_error = false; + dateStr += 'T' + $filter('schZeroPad')(scope.schedulerStartHour, 2) + ':' + $filter('schZeroPad')(scope.schedulerStartMinute, 2) + ':' + + $filter('schZeroPad')(scope.schedulerStartSecond, 2) + '.000Z'; + scope.schedulerUTCTime = $filter('schDateStrFix')($timezones.toUTC(dateStr, scope.schedulerTimeZone.name).toISOString()); + scope.scheduler_form_schedulerStartDt_error = false; + scope.scheduler_startTime_error = false; + } catch (e) { + scope.startDateError("Provide a valid start date and time"); + } + } else { scope.scheduler_startTime_error = false; - } - catch(e) { - scope.startDateError("Provide a valid start date and time"); + scope.scheduler_form_schedulerStartDt_error = false; + scope.schedulerUTCTime = $filter('schDateStrFix')(scope.schedulerStartDt + 'T' + scope.schedulerStartHour + ':' + scope.schedulerStartMinute + + ':' + scope.schedulerStartSecond + '.000Z'); } } - else { - scope.scheduler_startTime_error = false; - scope.scheduler_form_schedulerStartDt_error = false; - scope.schedulerUTCTime = $filter('schDateStrFix')(scope.schedulerStartDt + 'T' + scope.schedulerStartHour + ':' + scope.schedulerStartMinute + - ':' + scope.schedulerStartSecond + '.000Z'); + if (callback) { + callback(); } - } - if (callback){ - callback(); - } - }; + }; - // change the utc time with the new start date - scope.$watch('schedulerStartDt', function() { - scope.scheduleTimeChange(scope.processSchedulerEndDt); - }); + // change the utc time with the new start date + scope.$watch('schedulerStartDt', function() { + scope.scheduleTimeChange(scope.processSchedulerEndDt); + }); - scope.resetError = function(variable) { - scope[variable] = false; - }; + scope.resetError = function(variable) { + scope[variable] = false; + }; - scope.scheduleRepeatChange = function() { - // reset the week buttons and scope values to be empty - // when the schedule repeat is changed to week - if (scope.schedulerFrequency.name === "Week") { - scope.weekDays = []; - delete scope.weekDaySUClass; - delete scope.weekDayMOClass; - delete scope.weekDayTUClass; - delete scope.weekDayWEClass; - delete scope.weekDayTHClass; - delete scope.weekDayFRClass; - delete scope.weekDaySAClass; - } - if (scope.schedulerFrequency && scope.schedulerFrequency.value !== '' && scope.schedulerFrequency.value !== 'none') { - scope.schedulerInterval = 1; - scope.schedulerShowInterval = true; - scope.schedulerIntervalLabel = scope.schedulerFrequency.intervalLabel; - } - else { - scope.schedulerShowInterval = false; - scope.schedulerEnd = scope.endOptions[0]; - } - scope.sheduler_frequency_error = false; - scope.$emit("updateSchedulerSelects"); - }; - - scope.showCalendar = function(fld) { - $('#' + fld).focus(); - }; - - scope.monthlyRepeatChange = function() { - if (scope.monthlyRepeatOption !== 'day') { - $('#monthDay').spinner('disable'); - } - else { - $('#monthDay').spinner('enable'); - } - }; - - scope.yearlyRepeatChange = function() { - if (scope.yearlyRepeatOption !== 'month') { - $('#yearlyRepeatDay').spinner('disable'); - } - else { - $('#yearlyRepeatDay').spinner('enable'); - } - }; - - scope.setWeekday = function(event, day) { - // Add or remove day when user clicks checkbox button - var i = scope.weekDays.indexOf(day); - if (i >= 0) { - scope.weekDays.splice(i,1); - } - else { - scope.weekDays.push(day); - } - $(event.target).blur(); - scope.scheduler_weekDays_error = false; - }; - - scope.startDateError = function(msg) { - if (scope.scheduler_form) { - if (scope.scheduler_form.schedulerStartDt) { - scope.scheduler_form_schedulerStartDt_error = msg; - scope.scheduler_form.schedulerStartDt.$pristine = false; - scope.scheduler_form.schedulerStartDt.$dirty = true; + scope.scheduleRepeatChange = function() { + // reset the week buttons and scope values to be empty + // when the schedule repeat is changed to week + if (scope.schedulerFrequency.name === "Week") { + scope.weekDays = []; + delete scope.weekDaySUClass; + delete scope.weekDayMOClass; + delete scope.weekDayTUClass; + delete scope.weekDayWEClass; + delete scope.weekDayTHClass; + delete scope.weekDayFRClass; + delete scope.weekDaySAClass; } - $('#schedulerStartDt').removeClass('ng-pristine').removeClass('ng-valid').removeClass('ng-valid-custom-error') - .addClass('ng-dirty').addClass('ng-invalid').addClass('ng-invalid-custom-error'); - } - }; - - scope.resetStartDate = function() { - if (scope.scheduler_form) { - scope.scheduler_form_schedulerStartDt_error = ''; - if (scope.scheduler_form.schedulerStartDt) { - scope.scheduler_form.schedulerStartDt.$setValidity('custom-error', true); - scope.scheduler_form.schedulerStartDt.$setPristine(); + if (scope.schedulerFrequency && scope.schedulerFrequency.value !== '' && scope.schedulerFrequency.value !== 'none') { + scope.schedulerInterval = 1; + scope.schedulerShowInterval = true; + scope.schedulerIntervalLabel = scope.schedulerFrequency.intervalLabel; + } else { + scope.schedulerShowInterval = false; + scope.schedulerEnd = scope.endOptions[0]; } + scope.sheduler_frequency_error = false; + scope.$emit("updateSchedulerSelects"); + }; + + scope.showCalendar = function(fld) { + $('#' + fld).focus(); + }; + + scope.monthlyRepeatChange = function() { + if (scope.monthlyRepeatOption !== 'day') { + $('#monthDay').spinner('disable'); + } else { + $('#monthDay').spinner('enable'); + } + }; + + scope.yearlyRepeatChange = function() { + if (scope.yearlyRepeatOption !== 'month') { + $('#yearlyRepeatDay').spinner('disable'); + } else { + $('#yearlyRepeatDay').spinner('enable'); + } + }; + + scope.setWeekday = function(event, day) { + // Add or remove day when user clicks checkbox button + var i = scope.weekDays.indexOf(day); + if (i >= 0) { + scope.weekDays.splice(i, 1); + } else { + scope.weekDays.push(day); + } + $(event.target).blur(); + scope.scheduler_weekDays_error = false; + }; + + scope.startDateError = function(msg) { + if (scope.scheduler_form) { + if (scope.scheduler_form.schedulerStartDt) { + scope.scheduler_form_schedulerStartDt_error = msg; + scope.scheduler_form.schedulerStartDt.$pristine = false; + scope.scheduler_form.schedulerStartDt.$dirty = true; + } + $('#schedulerStartDt').removeClass('ng-pristine').removeClass('ng-valid').removeClass('ng-valid-custom-error') + .addClass('ng-dirty').addClass('ng-invalid').addClass('ng-invalid-custom-error'); + } + }; + + scope.resetStartDate = function() { + if (scope.scheduler_form) { + scope.scheduler_form_schedulerStartDt_error = ''; + if (scope.scheduler_form.schedulerStartDt) { + scope.scheduler_form.schedulerStartDt.$setValidity('custom-error', true); + scope.scheduler_form.schedulerStartDt.$setPristine(); + } + } + }; + + scope.schedulerEndChange = function(key, value) { + scope[key] = $filter('schZeroPad')(parseInt(value), 2); + }; + + // When timezones become available, use to set defaults + if (scope.removeZonesReady) { + scope.removeZonesReady(); } + scope.removeZonesReady = scope.$on('zonesReady', function() { + scope.timeZones = JSON.parse(localStorage.zones); + scope.setDefaults(); + }); + + if (useTimezone) { + // Build list of timezone element options - $timezones.getZoneList(scope); - } - else { - scope.setDefaults(); - } - - return CreateObject(scope, requireFutureStartTime); - - }; - }]) + } + ]) /** Return an AngularScheduler object we can use to get the RRule result from user input, check if @@ -220,280 +228,272 @@ angular.module('AngularScheduler', ['underscore']) scheduler widget */ .factory('CreateObject', ['AngularScheduler.useTimezone', '$filter', 'GetRule', 'Inject', 'InjectDetail', 'SetDefaults', '$timezones', 'SetRule', 'InRange', - function(useTimezone, $filter, GetRule, Inject, InjectDetail, SetDefaults, $timezones, SetRule, InRange) { - return function(scope, requireFutureST) { - var fn = function() { + function(useTimezone, $filter, GetRule, Inject, InjectDetail, SetDefaults, $timezones, SetRule, InRange) { + return function(scope, requireFutureST) { + var fn = function() { - this.scope = scope; - this.useTimezone = useTimezone; - this.requireFutureStartTime = requireFutureST; + this.scope = scope; + this.useTimezone = useTimezone; + this.requireFutureStartTime = requireFutureST; - // Evaluate user intput and build options for passing to rrule - this.getOptions = function() { - var options = {}; - options.startDate = this.scope.schedulerUTCTime; - options.frequency = this.scope.schedulerFrequency.value; - options.interval = parseInt(this.scope.schedulerInterval); - if (this.scope.schedulerEnd.value === 'after') { - options.occurrenceCount = this.scope.schedulerOccurrenceCount; - } - if (this.scope.schedulerEnd.value === 'on') { - options.endDate = scope.schedulerEndDt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, function(match, p1, p2, p3) { + // Evaluate user intput and build options for passing to rrule + this.getOptions = function() { + var options = {}; + options.startDate = this.scope.schedulerUTCTime; + options.frequency = this.scope.schedulerFrequency.value; + options.interval = parseInt(this.scope.schedulerInterval); + if (this.scope.schedulerEnd.value === 'after') { + options.occurrenceCount = this.scope.schedulerOccurrenceCount; + } + if (this.scope.schedulerEnd.value === 'on') { + options.endDate = scope.schedulerEndDt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, function(match, p1, p2, p3) { return p3 + '-' + p1 + '-' + p2; }) + 'T' + - $filter('schZeroPad')(this.scope.schedulerEndHour,2) + ':' + - $filter('schZeroPad')(this.scope.schedulerEndMinute,2) + ':' + - $filter('schZeroPad')(this.scope.schedulerEndSecond,2)+ 'Z'; - } - if (this.scope.schedulerFrequency.value === 'weekly') { - options.weekDays = this.scope.weekDays; - } - else if (this.scope.schedulerFrequency.value === 'yearly') { - if (this.scope.yearlyRepeatOption === 'month') { - options.month = this.scope.yearlyMonth.value; - options.monthDay = this.scope.yearlyMonthDay; + $filter('schZeroPad')(this.scope.schedulerEndHour, 2) + ':' + + $filter('schZeroPad')(this.scope.schedulerEndMinute, 2) + ':' + + $filter('schZeroPad')(this.scope.schedulerEndSecond, 2) + 'Z'; } - else { - options.setOccurrence = this.scope.yearlyOccurrence.value; - options.weekDays = this.scope.yearlyWeekDay.value; - options.month = this.scope.yearlyOtherMonth.value; - } - } - else if (this.scope.schedulerFrequency.value === 'monthly') { - if (this.scope.monthlyRepeatOption === 'day') { - options.monthDay = this.scope.monthDay; - } - else { - options.setOccurrence = this.scope.monthlyOccurrence.value; - options.weekDays = this.scope.monthlyWeekDay.value; - } - } - return options; - }; - - // Clear custom field errors - this.clearErrors = function() { - this.scope.scheduler_weekDays_error = false; - this.scope.scheduler_endDt_error = false; - this.scope.resetStartDate(); - this.scope.scheduler_endDt_error = false; - this.scope.scheduler_interval_error = false; - this.scope.scheduler_occurrenceCount_error = false; - this.scope.scheduler_monthDay_error = false; - this.scope.scheduler_yearlyMonthDay_error = false; - - if (this.scope.scheduler_form && this.scope.scheduler_form.schedulerEndDt) { - this.scope.scheduler_form.schedulerEndDt.$setValidity('custom-error', true); - this.scope.scheduler_form.schedulerEndDt.$setPristine(); - this.scope.scheduler_form.$setPristine(); - } - }; - - // Set values for detail page - this.setDetails = function() { - var rrule = this.getRRule(), - scope = this.scope; - if (rrule) { - scope.rrule_nlp_description = rrule.toText(); - scope.dateChoice = 'local'; - scope.occurrence_list = []; - rrule.all(function(date, i){ - var local, dt; - if (i < 10) { - if (useTimezone) { - dt = $timezones.align(date, scope.schedulerTimeZone.name); - local = $filter('schZeroPad')(dt.getMonth() + 1,2) + '/' + - $filter('schZeroPad')(dt.getDate(),2) + '/' + dt.getFullYear() + ' ' + - $filter('schZeroPad')(dt.getHours(),2) + ':' + - $filter('schZeroPad')(dt.getMinutes(),2) + ':' + - $filter('schZeroPad')(dt.getSeconds(),2) + ' ' + - dt.getTimezoneAbbreviation(); - } - else { - local = $filter('date')(date, 'MM/dd/yyyy HH:mm:ss Z'); - } - scope.occurrence_list.push({ utc: $filter('schDateStrFix')(date.toISOString()), local: local }); - return true; + if (this.scope.schedulerFrequency.value === 'weekly') { + options.weekDays = this.scope.weekDays; + } else if (this.scope.schedulerFrequency.value === 'yearly') { + if (this.scope.yearlyRepeatOption === 'month') { + options.month = this.scope.yearlyMonth.value; + options.monthDay = this.scope.yearlyMonthDay; + } else { + options.setOccurrence = this.scope.yearlyOccurrence.value; + options.weekDays = this.scope.yearlyWeekDay.value; + options.month = this.scope.yearlyOtherMonth.value; } - return false; - }); - scope.rrule_nlp_description = rrule.toText().replace(/^RRule error.*$/,'Natural language description not available'); - scope.rrule = rrule.toString(); - } - }; + } else if (this.scope.schedulerFrequency.value === 'monthly') { + if (this.scope.monthlyRepeatOption === 'day') { + options.monthDay = this.scope.monthDay; + } else { + options.setOccurrence = this.scope.monthlyOccurrence.value; + options.weekDays = this.scope.monthlyWeekDay.value; + } + } + return options; + }; - // Check the input form for errors - this.isValid = function() { - var startDt, now, dateStr, adjNow, timeNow, timeFuture, validity = true; - this.clearErrors(); + // Clear custom field errors + this.clearErrors = function() { + this.scope.scheduler_weekDays_error = false; + this.scope.scheduler_endDt_error = false; + this.scope.resetStartDate(); + this.scope.scheduler_endDt_error = false; + this.scope.scheduler_interval_error = false; + this.scope.scheduler_occurrenceCount_error = false; + this.scope.scheduler_monthDay_error = false; + this.scope.scheduler_yearlyMonthDay_error = false; - if (this.scope.schedulerFrequency.value !== 'none' && !InRange(this.scope.schedulerInterval, 1, 999, 3)) { - this.scope.scheduler_interval_error = true; - validity = false; - } + if (this.scope.scheduler_form && this.scope.scheduler_form.schedulerEndDt) { + this.scope.scheduler_form.schedulerEndDt.$setValidity('custom-error', true); + this.scope.scheduler_form.schedulerEndDt.$setPristine(); + this.scope.scheduler_form.$setPristine(); + } + }; - if (this.scope.schedulerEnd.value === 'after' && !InRange(this.scope.schedulerOccurrenceCount, 1, 999, 3)) { - this.scope.scheduler_occurrenceCount_error = true; - validity = false; - } + // Set values for detail page + this.setDetails = function() { + var rrule = this.getRRule(), + scope = this.scope; + if (rrule) { + scope.rrule_nlp_description = rrule.toText(); + scope.dateChoice = 'local'; + scope.occurrence_list = []; + rrule.all(function(date, i) { + var local, dt; + if (i < 10) { + if (useTimezone) { + dt = $timezones.align(date, scope.schedulerTimeZone.name); + local = $filter('schZeroPad')(dt.getMonth() + 1, 2) + '/' + + $filter('schZeroPad')(dt.getDate(), 2) + '/' + dt.getFullYear() + ' ' + + $filter('schZeroPad')(dt.getHours(), 2) + ':' + + $filter('schZeroPad')(dt.getMinutes(), 2) + ':' + + $filter('schZeroPad')(dt.getSeconds(), 2) + ' ' + + dt.getTimezoneAbbreviation(); + } else { + local = $filter('date')(date, 'MM/dd/yyyy HH:mm:ss Z'); + } + scope.occurrence_list.push({ utc: $filter('schedulerDate')($filter('schDateStrFix')(date.toISOString())), local: $filter('schedulerDate')(local) }); + return true; + } + return false; + }); + scope.rrule_nlp_description = rrule.toText().replace(/^RRule error.*$/, 'Natural language description not available'); + scope.rrule = rrule.toString(); + } + }; - if (this.scope.schedulerFrequency.value === 'weekly' && this.scope.weekDays.length === 0) { - this.scope.scheduler_weekDays_error = true; - validity = false; - } + // Check the input form for errors + this.isValid = function() { + var startDt, now, dateStr, adjNow, timeNow, timeFuture, validity = true; + this.clearErrors(); - if (this.scope.schedulerFrequency.value === 'monthly' && this.scope.monthlyRepeatOption === 'day' && !InRange(this.scope.monthDay, 1, 31, 99)) { - this.scope.scheduler_monthDay_error = true; - validity = false; - } - - if (this.scope.schedulerFrequency.value === 'yearly' && this.scope.yearlyRepeatOption === 'month' && !InRange(this.scope.yearlyMonthDay, 1, 31, 99)) { - this.scope.scheduler_yearlyMonthDay_error = true; - validity = false; - } - if ( !(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2)) ) { - this.scope.scheduler_startTime_error = true; - validity = false; - } - if (!this.scope.scheduler_form.schedulerName.$valid) { - // Make sure schedulerName requird error shows up - this.scope.scheduler_form.schedulerName.$dirty = true; - $('#schedulerName').addClass('ng-dirty'); - validity = false; - } - if (this.scope.schedulerEnd.value === 'on') { - if (!/^\d{2}\/\d{2}\/\d{4}$/.test(this.scope.schedulerEndDt)) { - this.scope.scheduler_form.schedulerEndDt.$pristine = false; - this.scope.scheduler_form.schedulerEndDt.$dirty = true; - $('#schedulerEndDt').removeClass('ng-pristine').removeClass('ng-valid').removeClass('ng-valid-custom-error') - .addClass('ng-dirty').addClass('ng-invalid').addClass('ng-invalid-custom-error'); - this.scope.scheduler_endDt_error = true; + if (this.scope.schedulerFrequency.value !== 'none' && !InRange(this.scope.schedulerInterval, 1, 999, 3)) { + this.scope.scheduler_interval_error = true; validity = false; } - } - if (this.scope.schedulerUTCTime) { - try { - startDt = new Date(this.scope.schedulerUTCTime); - if (!isNaN(startDt)) { - timeFuture = startDt.getTime(); - now = new Date(); - if (this.useTimezone) { - dateStr = now.getFullYear() + '-' + - $filter('schZeroPad')(now.getMonth() + 1, 2)+ '-' + - $filter('schZeroPad')(now.getDate(),2) + 'T' + - $filter('schZeroPad')(now.getHours(),2) + ':' + - $filter('schZeroPad')(now.getMinutes(),2) + ':' + - $filter('schZeroPad')(now.getSeconds(),2) + '.000Z'; - adjNow = $timezones.toUTC(dateStr, this.scope.schedulerTimeZone.name); //Adjust to the selected TZ - timeNow = adjNow.getTime(); - } - else { - timeNow = now.getTime(); - } - if (this.requireFutureStartTime && timeNow >= timeFuture) { - this.scope.startDateError("Start time must be in the future"); - validity = false; - } - } - else { - this.scope.startDateError("Invalid start time"); + + if (this.scope.schedulerEnd.value === 'after' && !InRange(this.scope.schedulerOccurrenceCount, 1, 999, 3)) { + this.scope.scheduler_occurrenceCount_error = true; + validity = false; + } + + if (this.scope.schedulerFrequency.value === 'weekly' && this.scope.weekDays.length === 0) { + this.scope.scheduler_weekDays_error = true; + validity = false; + } + + if (this.scope.schedulerFrequency.value === 'monthly' && this.scope.monthlyRepeatOption === 'day' && !InRange(this.scope.monthDay, 1, 31, 99)) { + this.scope.scheduler_monthDay_error = true; + validity = false; + } + + if (this.scope.schedulerFrequency.value === 'yearly' && this.scope.yearlyRepeatOption === 'month' && !InRange(this.scope.yearlyMonthDay, 1, 31, 99)) { + this.scope.scheduler_yearlyMonthDay_error = true; + validity = false; + } + if (!(InRange(scope.schedulerStartHour, 0, 23, 2) && InRange(scope.schedulerStartMinute, 0, 59, 2) && InRange(scope.schedulerStartSecond, 0, 59, 2))) { + this.scope.scheduler_startTime_error = true; + validity = false; + } + if (!this.scope.scheduler_form.schedulerName.$valid) { + // Make sure schedulerName requird error shows up + this.scope.scheduler_form.schedulerName.$dirty = true; + $('#schedulerName').addClass('ng-dirty'); + validity = false; + } + if (this.scope.schedulerEnd.value === 'on') { + if (!/^\d{2}\/\d{2}\/\d{4}$/.test(this.scope.schedulerEndDt)) { + this.scope.scheduler_form.schedulerEndDt.$pristine = false; + this.scope.scheduler_form.schedulerEndDt.$dirty = true; + $('#schedulerEndDt').removeClass('ng-pristine').removeClass('ng-valid').removeClass('ng-valid-custom-error') + .addClass('ng-dirty').addClass('ng-invalid').addClass('ng-invalid-custom-error'); + this.scope.scheduler_endDt_error = true; validity = false; } } - catch(e) { - this.scope.startDateError("Invalid start time"); + if (this.scope.schedulerUTCTime) { + try { + startDt = new Date(this.scope.schedulerUTCTime); + if (!isNaN(startDt)) { + timeFuture = startDt.getTime(); + now = new Date(); + if (this.useTimezone) { + dateStr = now.getFullYear() + '-' + + $filter('schZeroPad')(now.getMonth() + 1, 2) + '-' + + $filter('schZeroPad')(now.getDate(), 2) + 'T' + + $filter('schZeroPad')(now.getHours(), 2) + ':' + + $filter('schZeroPad')(now.getMinutes(), 2) + ':' + + $filter('schZeroPad')(now.getSeconds(), 2) + '.000Z'; + adjNow = $timezones.toUTC(dateStr, this.scope.schedulerTimeZone.name); //Adjust to the selected TZ + timeNow = adjNow.getTime(); + } else { + timeNow = now.getTime(); + } + if (this.requireFutureStartTime && timeNow >= timeFuture) { + this.scope.startDateError("Start time must be in the future"); + validity = false; + } + } else { + this.scope.startDateError("Invalid start time"); + validity = false; + } + } catch (e) { + this.scope.startDateError("Invalid start time"); + validity = false; + } + } else { + this.scope.startDateError("Provide a start time"); validity = false; } - } - else { - this.scope.startDateError("Provide a start time"); - validity = false; - } - scope.schedulerIsValid = validity; - if (validity) { - this.setDetails(); - } + scope.schedulerIsValid = validity; + if (validity) { + this.setDetails(); + } - return validity; - }; + return validity; + }; - var that = this; + var that = this; - that.scope.$on("loadSchedulerDetailPane", function() { - that.isValid(); - }); + that.scope.$on("loadSchedulerDetailPane", function() { + that.isValid(); + }); - // Returns an rrule object - this.getRRule = function() { - var options = this.getOptions(); - return GetRule(options); - }; + // Returns an rrule object + this.getRRule = function() { + var options = this.getOptions(); + return GetRule(options); + }; - // Return object containing schedule name, string representation of rrule per iCalendar RFC, - // and options used to create rrule - this.getValue = function() { - var rule = this.getRRule(), - options = this.getOptions(); - return { - name: scope.schedulerName, - rrule: rule.toString(), - options: options + // Return object containing schedule name, string representation of rrule per iCalendar RFC, + // and options used to create rrule + this.getValue = function() { + var rule = this.getRRule(), + options = this.getOptions(); + return { + name: scope.schedulerName, + rrule: rule.toString(), + options: options + }; + }; + + this.setRRule = function(rule) { + this.clear(); + return SetRule(rule, this.scope); + }; + + this.setName = function(name) { + this.scope.schedulerName = name; + }; + + // Read in the HTML partial, compile and inject it into the DOM. + // Pass in the target element's id attribute value or an angular.element() + // object. + this.inject = function(element, showButtons) { + return Inject({ scope: this.scope, target: element, buttons: showButtons }); + }; + + this.injectDetail = function(element, showRRule) { + return InjectDetail({ scope: this.scope, target: element, showRRule: showRRule }); + }; + + // Clear the form, returning all elements to a default state + this.clear = function() { + this.clearErrors(); + if (this.scope.scheduler_form && this.scope.scheduler_form.schedulerName) { + this.scope.scheduler_form.schedulerName.$setPristine(); + } + this.scope.setDefaults(); + }; + + // Get the user's local timezone + this.getUserTimezone = function() { + return $timezones.getLocal(); + }; + + // futureStartTime setter/getter + this.setRequireFutureStartTime = function(opt) { + this.requireFutureStartTime = opt; + }; + + this.getRequireFutureStartTime = function() { + return this.requireFutureStartTime; + }; + + this.setShowRRule = function(opt) { + scope.showRRule = opt; }; }; - - this.setRRule = function(rule) { - this.clear(); - return SetRule(rule, this.scope); - }; - - this.setName = function(name) { - this.scope.schedulerName = name; - }; - - // Read in the HTML partial, compile and inject it into the DOM. - // Pass in the target element's id attribute value or an angular.element() - // object. - this.inject = function(element, showButtons) { - return Inject({ scope: this.scope, target: element, buttons: showButtons }); - }; - - this.injectDetail = function(element, showRRule) { - return InjectDetail({ scope: this.scope, target: element, showRRule: showRRule }); - }; - - // Clear the form, returning all elements to a default state - this.clear = function() { - this.clearErrors(); - if (this.scope.scheduler_form && this.scope.scheduler_form.schedulerName) { - this.scope.scheduler_form.schedulerName.$setPristine(); - } - this.scope.setDefaults(); - }; - - // Get the user's local timezone - this.getUserTimezone = function() { - return $timezones.getLocal(); - }; - - // futureStartTime setter/getter - this.setRequireFutureStartTime = function(opt) { - this.requireFutureStartTime = opt; - }; - - this.getRequireFutureStartTime = function() { - return this.requireFutureStartTime; - }; - - this.setShowRRule = function(opt) { - scope.showRRule = opt; - }; + return new fn(); }; - return new fn(); - }; - }]) + } + ]) - .factory('InRange', [ function() { + .factory('InRange', [function() { return function(x, min, max, length) { var rx = new RegExp("\\d{1," + length + "}"); if (!rx.test(x)) { @@ -526,11 +526,11 @@ angular.module('AngularScheduler', ['underscore']) }); $http({ method: 'GET', url: scheduler_partial + 'angular-scheduler.html' }) - .success( function(data) { + .success(function(data) { scope.$emit('htmlReady', data); }) - .error( function(data, status) { - throw('Error reading ' + scheduler_partial + 'angular-scheduler.html. ' + status); + .error(function(data, status) { + throw ('Error reading ' + scheduler_partial + 'angular-scheduler.html. ' + status); //$log.error('Error calling ' + scheduler_partial + '. ' + status); }); }; @@ -555,11 +555,11 @@ angular.module('AngularScheduler', ['underscore']) }); $http({ method: 'GET', url: scheduler_partial + 'angular-scheduler-detail.html' }) - .success( function(data) { + .success(function(data) { scope.$emit('htmlDetailReady', data); }) - .error( function(data, status) { - throw('Error reading ' + scheduler_partial + 'angular-scheduler-detail.html. ' + status); + .error(function(data, status) { + throw ('Error reading ' + scheduler_partial + 'angular-scheduler-detail.html. ' + status); //$log.error('Error calling ' + scheduler_partial + '. ' + status); }); }; @@ -570,26 +570,25 @@ angular.module('AngularScheduler', ['underscore']) // Convert user inputs to an rrule. Returns rrule object using https://github.com/jkbr/rrule // **list of 'valid values' found below in LoadLookupValues - var startDate = params.startDate, // date object or string in yyyy-MM-ddTHH:mm:ss.sssZ format - frequency = params.frequency, // string, optional, valid value from frequencyOptions - interval = params.interval, // integer, optional - occurrenceCount = params.occurrenceCount, //integer, optional - endDate = params.endDate, // date object or string in yyyy-MM-dd format, optional - // ignored if occurrenceCount provided - month = params.month, // integer, optional, valid value from months - monthDay = params.monthDay, // integer, optional, between 1 and 31 - weekDays = params.weekDays, // integer, optional, valid value from weekdays + var startDate = params.startDate, // date object or string in yyyy-MM-ddTHH:mm:ss.sssZ format + frequency = params.frequency, // string, optional, valid value from frequencyOptions + interval = params.interval, // integer, optional + occurrenceCount = params.occurrenceCount, //integer, optional + endDate = params.endDate, // date object or string in yyyy-MM-dd format, optional + // ignored if occurrenceCount provided + month = params.month, // integer, optional, valid value from months + monthDay = params.monthDay, // integer, optional, between 1 and 31 + weekDays = params.weekDays, // integer, optional, valid value from weekdays setOccurrence = params.setOccurrence, // integer, optional, valid value from occurrences - options = {}, i; + options = {}, + i; if (angular.isDate(startDate)) { options.dtstart = startDate; - } - else { + } else { try { options.dtstart = new Date(startDate); - } - catch(e) { + } catch (e) { $log.error('Date conversion failed. Attempted to convert ' + startDate + ' to Date. ' + e.message); } } @@ -604,7 +603,7 @@ angular.module('AngularScheduler', ['underscore']) if (weekDays && angular.isArray(weekDays)) { options.byweekday = []; - for (i=0; i < weekDays.length; i++) { + for (i = 0; i < weekDays.length; i++) { options.byweekday.push(RRule[weekDays[i].toUpperCase()]); } } @@ -623,22 +622,18 @@ angular.module('AngularScheduler', ['underscore']) if (occurrenceCount) { options.count = occurrenceCount; - } - else if (endDate) { + } else if (endDate) { if (angular.isDate(endDate)) { options.until = endDate; - } - else { + } else { try { options.until = new Date(endDate); - } - catch(e) { + } catch (e) { $log.error('Date conversion failed. Attempted to convert ' + endDate + ' to Date. ' + e.message); } } } - } - else { + } else { // We only want to run 1x options.freq = RRule.DAILY; options.interval = 1; @@ -648,250 +643,240 @@ angular.module('AngularScheduler', ['underscore']) }; }]) - .factory('SetRule', ['AngularScheduler.useTimezone', '_', '$log', '$timezones', '$filter', - function(useTimezone, _, $log, $timezones, $filter) { - return function(rule, scope) { - var set, result = '', i, - setStartDate = false; + .factory('SetRule', ['AngularScheduler.useTimezone', '$log', '$timezones', '$filter', + function(useTimezone, $log, $timezones, $filter) { + return function(rule, scope) { + var set, result = '', + i, + setStartDate = false; - // Search the set of RRule keys for a particular key, returning its value - function getValue(set, key) { - var pair = _.find(set, function(x) { - var k = x.split(/=/)[0].toUpperCase(); - return (k === key); - }); - if (pair) { - return pair.split(/=/)[1].toUpperCase(); - } - return null; - } - - function toWeekDays(days) { - var darray = days.toLowerCase().split(/,/), - match = _.find(scope.weekdays, function(x) { - var warray = (angular.isArray(x.value)) ? x.value : [x.value], - diffA = _.difference(warray, darray), - diffB = _.difference(darray, warray); - return (diffA.length === 0 && diffB.length === 0); + // Search the set of RRule keys for a particular key, returning its value + function getValue(set, key) { + var pair = _.find(set, function(x) { + var k = x.split(/=/)[0].toUpperCase(); + return (k === key); }); - return match; - } - - function setValue(pair, set) { - var key = pair.split(/=/)[0].toUpperCase(), - value = pair.split(/=/)[1], - days, l, j, dt, month, day, timeString; - - if (key === 'NAME') { - //name is not actually part of RRule, but we can handle it just the same - scope.schedulerName = value; + if (pair) { + return pair.split(/=/)[1].toUpperCase(); + } + return null; } - if (key === 'FREQ') { - l = value.toLowerCase(); - scope.schedulerFrequency = _.find(scope.frequencyOptions, function(opt) { - scope.schedulerIntervalLabel = opt.intervalLabel; - return opt.value === l; - }); - if (!scope.schedulerFrequency || !scope.schedulerFrequency.name) { - result = 'FREQ not found in list of valid options'; - } + function toWeekDays(days) { + var darray = days.toLowerCase().split(/,/), + match = _.find(scope.weekdays, function(x) { + var warray = (angular.isArray(x.value)) ? x.value : [x.value], + diffA = _.difference(warray, darray), + diffB = _.difference(darray, warray); + return (diffA.length === 0 && diffB.length === 0); + }); + return match; } - if (key === 'INTERVAL') { - if (parseInt(value,10)) { - scope.schedulerInterval = parseInt(value,10); - scope.schedulerShowInterval = true; + + function setValue(pair, set) { + var key = pair.split(/=/)[0].toUpperCase(), + value = pair.split(/=/)[1], + days, l, j, dt, month, day, timeString; + + if (key === 'NAME') { + //name is not actually part of RRule, but we can handle it just the same + scope.schedulerName = value; } - else { - result = 'INTERVAL must contain an integer > 0'; + + if (key === 'FREQ') { + l = value.toLowerCase(); + scope.schedulerFrequency = _.find(scope.frequencyOptions, function(opt) { + scope.schedulerIntervalLabel = opt.intervalLabel; + return opt.value === l; + }); + if (!scope.schedulerFrequency || !scope.schedulerFrequency.name) { + result = 'FREQ not found in list of valid options'; + } } - } - if (key === 'BYDAY') { - if (getValue(set, 'FREQ') === 'WEEKLY') { - days = value.split(/,/); - scope.weekDays = []; - for (j=0; j < days.length; j++) { - if (_.contains(['SU','MO','TU','WE','TH','FR','SA'], days[j])) { - scope.weekDays.push(days[j].toLowerCase()); - scope['weekDay' + days[j].toUpperCase() + 'Class'] = 'active'; //activate related button + if (key === 'INTERVAL') { + if (parseInt(value, 10)) { + scope.schedulerInterval = parseInt(value, 10); + scope.schedulerShowInterval = true; + } else { + result = 'INTERVAL must contain an integer > 0'; + } + } + if (key === 'BYDAY') { + if (getValue(set, 'FREQ') === 'WEEKLY') { + days = value.split(/,/); + scope.weekDays = []; + for (j = 0; j < days.length; j++) { + if (_.contains(['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'], days[j])) { + scope.weekDays.push(days[j].toLowerCase()); + scope['weekDay' + days[j].toUpperCase() + 'Class'] = 'active'; //activate related button + } else { + result = 'BYDAY contains unrecognized day value(s)'; + } } - else { + } else if (getValue(set, 'FREQ') === 'MONTHLY') { + scope.monthlyRepeatOption = 'other'; + scope.monthlyWeekDay = toWeekDays(value); + if (!scope.monthlyWeekDay) { + result = 'BYDAY contains unrecognized day value(s)'; + } + } else { + scope.yearlyRepeatOption = 'other'; + scope.yearlyWeekDay = toWeekDays(value); + if (!scope.yearlyWeekDay) { result = 'BYDAY contains unrecognized day value(s)'; } } } - else if (getValue(set, 'FREQ') === 'MONTHLY') { - scope.monthlyRepeatOption = 'other'; - scope.monthlyWeekDay = toWeekDays(value); - if (!scope.monthlyWeekDay) { - result = 'BYDAY contains unrecognized day value(s)'; + if (key === 'BYMONTHDAY') { + if (parseInt(value, 10) && parseInt(value, 10) > 0 && parseInt(value, 10) < 32) { + scope.monthDay = parseInt(value, 10); + scope.monhthlyRepeatOption = 'day'; + } else { + result = 'BYMONTHDAY must contain an integer between 1 and 31'; } } - else { - scope.yearlyRepeatOption = 'other'; - scope.yearlyWeekDay = toWeekDays(value); - if (!scope.yearlyWeekDay) { - result = 'BYDAY contains unrecognized day value(s)'; + if (key === 'DTSTART') { + // The form has been reset to the local zone + setStartDate = true; + if (/\d{8}T\d{6}.*Z/.test(value)) { + // date may come in without separators. add them so new Date constructor will work + value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/, + function(match, p1, p2, p3, p4, p5, p6) { + return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0, 2) + 'Z'; + }); } - } - } - if (key === 'BYMONTHDAY') { - if (parseInt(value,10) && parseInt(value,10) > 0 && parseInt(value,10) < 32) { - scope.monthDay = parseInt(value,10); - scope.monhthlyRepeatOption = 'day'; - } - else { - result = 'BYMONTHDAY must contain an integer between 1 and 31'; - } - } - if (key === 'DTSTART') { - // The form has been reset to the local zone - setStartDate = true; - if (/\d{8}T\d{6}.*Z/.test(value)) { - // date may come in without separators. add them so new Date constructor will work - value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/, - function(match, p1, p2, p3, p4,p5,p6) { - return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0,2) + 'Z'; - }); - } - if (useTimezone) { - dt = new Date(value); // date adjusted to local zone automatically - month = $filter('schZeroPad')(dt.getMonth() + 1, 2); - day = $filter('schZeroPad')(dt.getDate(), 2); - scope.schedulerStartDt = month + '/' + day + '/' + dt.getFullYear(); - scope.schedulerStartHour = $filter('schZeroPad')(dt.getHours(),2); - scope.schedulerStartMinute = $filter('schZeroPad')(dt.getMinutes(),2); - scope.schedulerStartSecond = $filter('schZeroPad')(dt.getSeconds(),2); - scope.scheduleTimeChange(); // calc UTC - } - else { - // expects inbound dates to be in ISO format: 2014-04-02T00:00:00.000Z - scope.schedulerStartDt = value.replace(/T.*$/,'').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) { + if (useTimezone) { + dt = new Date(value); // date adjusted to local zone automatically + month = $filter('schZeroPad')(dt.getMonth() + 1, 2); + day = $filter('schZeroPad')(dt.getDate(), 2); + scope.schedulerStartDt = month + '/' + day + '/' + dt.getFullYear(); + scope.schedulerStartHour = $filter('schZeroPad')(dt.getHours(), 2); + scope.schedulerStartMinute = $filter('schZeroPad')(dt.getMinutes(), 2); + scope.schedulerStartSecond = $filter('schZeroPad')(dt.getSeconds(), 2); + scope.scheduleTimeChange(); // calc UTC + } else { + // expects inbound dates to be in ISO format: 2014-04-02T00:00:00.000Z + scope.schedulerStartDt = value.replace(/T.*$/, '').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) { return p2 + '/' + p3 + '/' + p1; }); - timeString = value.replace(/^.*T/,''); - scope.schedulerStartHour = $filter('schZeroPad')(timeString.substr(0,2),2); - scope.schedulerStartMinute = $filter('schZeroPad')(timeString.substr(3,2),2); - scope.schedulerStartSecond = $filter('schZeroPad')(timeString.substr(6,2),2); - } - scope.scheduleTimeChange(); - } - if (key === 'BYSETPOS') { - if (getValue(set, 'FREQ') === 'YEARLY') { - scope.yearlRepeatOption = 'other'; - scope.yearlyOccurrence = _.find(scope.occurrences, function(x) { - return (x.value === parseInt(value,10)); - }); - if (!scope.yearlyOccurrence || !scope.yearlyOccurrence.name) { - result = 'BYSETPOS was not in the set of 1,2,3,4,-1'; + timeString = value.replace(/^.*T/, ''); + scope.schedulerStartHour = $filter('schZeroPad')(timeString.substr(0, 2), 2); + scope.schedulerStartMinute = $filter('schZeroPad')(timeString.substr(3, 2), 2); + scope.schedulerStartSecond = $filter('schZeroPad')(timeString.substr(6, 2), 2); } + scope.scheduleTimeChange(); } - else { - scope.monthlyOccurrence = _.find(scope.occurrences, function(x) { - return (x.value === parseInt(value,10)); - }); - if (!scope.monthlyOccurrence || !scope.monthlyOccurrence.name) { - result = 'BYSETPOS was not in the set of 1,2,3,4,-1'; - } - } - } - - if (key === 'COUNT') { - if (parseInt(value,10)) { - scope.schedulerEnd = scope.endOptions[1]; - scope.schedulerOccurrenceCount = parseInt(value,10); - } - else { - result = "COUNT must be a valid integer > 0"; - } - } - - if (key === 'UNTIL') { - if (/\d{8}T\d{6}.*Z/.test(value)) { - // date may come in without separators. add them so new Date constructor will work - value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/, - function(match, p1, p2, p3, p4,p5,p6) { - return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0,2) + 'Z'; + if (key === 'BYSETPOS') { + if (getValue(set, 'FREQ') === 'YEARLY') { + scope.yearlRepeatOption = 'other'; + scope.yearlyOccurrence = _.find(scope.occurrences, function(x) { + return (x.value === parseInt(value, 10)); }); + if (!scope.yearlyOccurrence || !scope.yearlyOccurrence.name) { + result = 'BYSETPOS was not in the set of 1,2,3,4,-1'; + } + } else { + scope.monthlyOccurrence = _.find(scope.occurrences, function(x) { + return (x.value === parseInt(value, 10)); + }); + if (!scope.monthlyOccurrence || !scope.monthlyOccurrence.name) { + result = 'BYSETPOS was not in the set of 1,2,3,4,-1'; + } + } } - scope.schedulerEnd = scope.endOptions[2]; - scope.schedulerEndDt = value.replace(/T.*$/,'').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) { + + if (key === 'COUNT') { + if (parseInt(value, 10)) { + scope.schedulerEnd = scope.endOptions[1]; + scope.schedulerOccurrenceCount = parseInt(value, 10); + } else { + result = "COUNT must be a valid integer > 0"; + } + } + + if (key === 'UNTIL') { + if (/\d{8}T\d{6}.*Z/.test(value)) { + // date may come in without separators. add them so new Date constructor will work + value = value.replace(/(\d{4})(\d{2})(\d{2}T)(\d{2})(\d{2})(\d{2}.*$)/, + function(match, p1, p2, p3, p4, p5, p6) { + return p1 + '-' + p2 + '-' + p3 + p4 + ':' + p5 + ':' + p6.substr(0, 2) + 'Z'; + }); + } + scope.schedulerEnd = scope.endOptions[2]; + scope.schedulerEndDt = value.replace(/T.*$/, '').replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, p1, p2, p3) { return p2 + '/' + p3 + '/' + p1; }); - timeString = value.replace(/^.*T/,''); - scope.schedulerEndHour = $filter('schZeroPad')(timeString.substr(0,2),2); - scope.schedulerEndMinute = $filter('schZeroPad')(timeString.substr(3,2),2); - scope.schedulerEndSecond = $filter('schZeroPad')(timeString.substr(6,2),2); - } + timeString = value.replace(/^.*T/, ''); + scope.schedulerEndHour = $filter('schZeroPad')(timeString.substr(0, 2), 2); + scope.schedulerEndMinute = $filter('schZeroPad')(timeString.substr(3, 2), 2); + scope.schedulerEndSecond = $filter('schZeroPad')(timeString.substr(6, 2), 2); + } - if (key === 'BYMONTH') { - if (getValue(set, 'FREQ') === 'YEARLY' && getValue(set, 'BYDAY')) { - scope.yearlRepeatOption = 'other'; - scope.yearlyOtherMonth = _.find(scope.months, function(x) { - return x.value === parseInt(value,10); - }); - if (!scope.yearlyOtherMonth || !scope.yearlyOtherMonth.name) { - result = 'BYMONTH must be an integer between 1 and 12'; + if (key === 'BYMONTH') { + if (getValue(set, 'FREQ') === 'YEARLY' && getValue(set, 'BYDAY')) { + scope.yearlRepeatOption = 'other'; + scope.yearlyOtherMonth = _.find(scope.months, function(x) { + return x.value === parseInt(value, 10); + }); + if (!scope.yearlyOtherMonth || !scope.yearlyOtherMonth.name) { + result = 'BYMONTH must be an integer between 1 and 12'; + } + } else { + scope.yearlyOption = 'month'; + scope.yearlyMonth = _.find(scope.months, function(x) { + return x.value === parseInt(value, 10); + }); + if (!scope.yearlyMonth || !scope.yearlyMonth.name) { + result = 'BYMONTH must be an integer between 1 and 12'; + } } } - else { - scope.yearlyOption = 'month'; - scope.yearlyMonth = _.find(scope.months, function(x) { - return x.value === parseInt(value,10); - }); - if (!scope.yearlyMonth || !scope.yearlyMonth.name) { - result = 'BYMONTH must be an integer between 1 and 12'; + + if (key === 'BYMONTHDAY') { + if (parseInt(value, 10)) { + scope.yearlyMonthDay = parseInt(value, 10); + } else { + result = 'BYMONTHDAY must be an integer between 1 and 31'; } } } - if (key === 'BYMONTHDAY') { - if (parseInt(value,10)) { - scope.yearlyMonthDay = parseInt(value,10); + function isValid() { + // Check what was put into scope vars, and see if anything is + // missing or not quite right. + if (scope.schedulerFrequency.name === 'weekly' && scope.weekDays.length === 0) { + result = 'Frequency is weekly, but BYDAYS value is missing.'; } - else { - result = 'BYMONTHDAY must be an integer between 1 and 31'; + if (!setStartDate) { + result = 'Warning: start date was not provided'; } } - } - function isValid() { - // Check what was put into scope vars, and see if anything is - // missing or not quite right. - if (scope.schedulerFrequency.name === 'weekly' && scope.weekDays.length === 0) { - result = 'Frequency is weekly, but BYDAYS value is missing.'; - } - if (!setStartDate) { - result = 'Warning: start date was not provided'; - } - } - - if (rule) { - set = rule.split(/;/); - if (angular.isArray(set)) { - for (i=0; i < set.length; i++) { - setValue(set[i], set); - if (result) { - break; + if (rule) { + set = rule.split(/;/); + if (angular.isArray(set)) { + for (i = 0; i < set.length; i++) { + setValue(set[i], set); + if (result) { + break; + } } + if (!result) { + isValid(); + } + } else { + result = 'No rule entered. Provide a valid RRule string.'; } - if (!result) { - isValid(); - } - } - else { + } else { result = 'No rule entered. Provide a valid RRule string.'; } - } - else { - result = 'No rule entered. Provide a valid RRule string.'; - } - if (result) { - $log.error(result); - } - return result; - }; - }]) + if (result) { + $log.error(result); + } + return result; + }; + } + ]) .factory('SetDefaults', ['$filter', function($filter) { return function(scope) { @@ -938,7 +923,7 @@ angular.module('AngularScheduler', ['underscore']) }; }]) - .factory('LoadLookupValues', [ function() { + .factory('LoadLookupValues', [function() { return function(scope) { scope.frequencyOptions = [ @@ -997,15 +982,15 @@ angular.module('AngularScheduler', ['underscore']) }]) // $filter('schZeroPad')(n, pad) -- or -- {{ n | afZeroPad:pad }} - .filter('schZeroPad', [ function() { - return function (n, pad) { - var str = (Math.pow(10,pad) + '').replace(/^1/,'') + (n + '').trim(); + .filter('schZeroPad', [function() { + return function(n, pad) { + var str = (Math.pow(10, pad) + '').replace(/^1/, '') + (n + '').trim(); return str.substr(str.length - pad); }; }]) // $filter('schdateStrFix')(s) where s is a date string in ISO format: yyyy-mm-ddTHH:MM:SS.sssZ. Returns string in format: mm/dd/yyyy HH:MM:SS UTC - .filter('schDateStrFix', [ function() { + .filter('schDateStrFix', [function() { return function(dateStr) { return dateStr.replace(/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*Z/, function(match, yy, mm, dd, hh, mi, ss) { return mm + '/' + dd + '/' + yy + ' ' + hh + ':' + mi + ':' + ss + ' UTC'; @@ -1013,7 +998,7 @@ angular.module('AngularScheduler', ['underscore']) }; }]) - .directive('schTooltip', [ function() { + .directive('schTooltip', [function() { return { link: function(scope, element, attrs) { var placement = (attrs.placement) ? attrs.placement : 'top'; @@ -1028,30 +1013,30 @@ angular.module('AngularScheduler', ['underscore']) }; }]) - .directive('schDatePicker', [ function() { + .directive('schDatePicker', [function() { return { require: 'ngModel', link: function(scope, element, attrs) { - var options = {}, - variable = attrs.ngModel, - defaultDate = new Date(); - options.dateFormat = attrs.dateFormat || 'mm/dd/yy'; - options.defaultDate = scope[variable]; - options.minDate = (attrs.minToday) ? defaultDate : null; - options.maxDate = (attrs.maxDate) ? new Date(attrs('maxDate')) : null; - options.changeMonth = (attrs.changeMonth === "false") ? false : true; - options.changeYear = (attrs.changeYear === "false") ? false : true; - options.beforeShow = function() { - setTimeout(function(){ - $('.ui-datepicker').css('z-index', 9999); - }, 100); - }; - $(element).datepicker(options); - } + var options = {}, + variable = attrs.ngModel, + defaultDate = new Date(); + options.dateFormat = attrs.dateFormat || 'mm/dd/yy'; + options.defaultDate = scope[variable]; + options.minDate = (attrs.minToday) ? defaultDate : null; + options.maxDate = (attrs.maxDate) ? new Date(attrs('maxDate')) : null; + options.changeMonth = (attrs.changeMonth === "false") ? false : true; + options.changeYear = (attrs.changeYear === "false") ? false : true; + options.beforeShow = function() { + setTimeout(function() { + $('.ui-datepicker').css('z-index', 9999); + }, 100); + }; + $(element).datepicker(options); + } }; }]) - // Custom directives + // Custom directives .directive('schSpinner', ['$filter', function($filter) { return { require: 'ngModel', @@ -1061,7 +1046,7 @@ angular.module('AngularScheduler', ['underscore']) zeroPad = attr.zeroPad, min = attr.min || 1, max = attr.max || 999; - $(element).spinner({ + element.spinner({ min: min, max: max, stop: function() { @@ -1069,10 +1054,9 @@ angular.module('AngularScheduler', ['underscore']) setTimeout(function() { scope.$apply(function() { if (zeroPad) { - scope[attr.ngModel] = $filter('schZeroPad')($(element).spinner('value'),zeroPad); - } - else { - scope[attr.ngModel] = $(element).spinner('value'); + scope[attr.ngModel] = $filter('schZeroPad')(element.spinner('value'), zeroPad); + } else { + scope[attr.ngModel] = element.spinner('value'); } if (attr.ngChange) { scope.$eval(attr.ngChange); @@ -1090,9 +1074,11 @@ angular.module('AngularScheduler', ['underscore']) } }); - $(element).on("click", function () { + $(element).on("click", function() { $(element).select(); }); } }; }]); + +})); From d7b79c5f08f437220bca16a94782418e2ea7c9c8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 24 Aug 2016 11:50:56 -0400 Subject: [PATCH 16/21] fixed locale --- .../client/lib/angular-scheduler/lib/angular-scheduler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js b/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js index b3d9d015c3..437dabe341 100644 --- a/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js +++ b/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js @@ -27,8 +27,8 @@ root.returnExports = factory(root._, root.angular, root.$, root.$.ui, root.moment); } }(this, function(_, angular, $, ui, moment) { - return angular.module('AngularScheduler', ['moment']) - .filter('schedulerDate', function() { + return angular.module('AngularScheduler', []) + .filter('schedulerDate', ['moment', function(moment) { return function(input) { var date; if(input === null){ @@ -38,7 +38,7 @@ return date.format('l') + input.slice(input.indexOf(" ")); } }; - }) + }]) .constant('AngularScheduler.partials', '/lib/') .constant('AngularScheduler.useTimezone', false) From 50ec9ca2599b2a0a1bdf9780e9c35a8f0789994d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 23 Aug 2016 15:35:43 -0400 Subject: [PATCH 17/21] temporarily pin the pytest version until the ldap error can be fixed --- Makefile | 9 +++++++++ requirements/requirements_dev.txt | 2 +- requirements/requirements_jenkins.txt | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b7e3fd9d21..6d3ada31fb 100644 --- a/Makefile +++ b/Makefile @@ -448,13 +448,22 @@ check: flake8 pep8 # pyflakes pylint TEST_DIRS=awx/main/tests # Run all API unit tests. test: + @if [ "$(VENV_BASE)" ]; then \ + . $(VENV_BASE)/tower/bin/activate; \ + fi; \ py.test $(TEST_DIRS) test_unit: + @if [ "$(VENV_BASE)" ]; then \ + . $(VENV_BASE)/tower/bin/activate; \ + fi; \ py.test awx/main/tests/unit # Run all API unit tests with coverage enabled. test_coverage: + @if [ "$(VENV_BASE)" ]; then \ + . $(VENV_BASE)/tower/bin/activate; \ + fi; \ py.test --create-db --cov=awx --cov-report=xml --junitxml=./reports/junit.xml $(TEST_DIRS) # Output test coverage as HTML (into htmlcov directory). diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index d986b1dc89..3e7cf4ae8c 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -5,7 +5,7 @@ unittest2 pep8 flake8 pyflakes==1.0.0 # Pinned until PR merges https://gitlab.com/pycqa/flake8/merge_requests/56 -pytest +pytest==2.9.2 pytest-cov pytest-django pytest-pythonpath diff --git a/requirements/requirements_jenkins.txt b/requirements/requirements_jenkins.txt index ff3fda270f..287a714939 100644 --- a/requirements/requirements_jenkins.txt +++ b/requirements/requirements_jenkins.txt @@ -6,7 +6,7 @@ pylint flake8 distribute==0.7.3 unittest2 -pytest +pytest==2.9.2 pytest-cov pytest-django pytest-pythonpath From dbe3f628d4e08e45234bf8b22f2744782933664e Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 25 Aug 2016 13:44:55 -0400 Subject: [PATCH 18/21] ensure team organizations are assigned to credentials --- .../migrations/0032_v302_credential_permissions_update.py | 1 + awx/main/migrations/_rbac.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/migrations/0032_v302_credential_permissions_update.py b/awx/main/migrations/0032_v302_credential_permissions_update.py index a961be6dcf..2587588e6d 100644 --- a/awx/main/migrations/0032_v302_credential_permissions_update.py +++ b/awx/main/migrations/0032_v302_credential_permissions_update.py @@ -25,5 +25,6 @@ class Migration(migrations.Migration): name='use_role', field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), ), + migrations.RunPython(rbac.infer_credential_org_from_team), migrations.RunPython(rbac.rebuild_role_hierarchy), ] diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index b60ac65691..245adc58ef 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -489,4 +489,7 @@ def rebuild_role_hierarchy(apps, schema_editor): logger.info('Rebuild completed in %f seconds' % (stop - start)) logger.info('Done.') - +def infer_credential_org_from_team(apps, schema_editor): + Credential = apps.get_model('main', "Credential") + for cred in Credential.objects.exclude(deprecated_team__isnull=True): + _update_credential_parents(cred.deprecated_team.organization, cred) From 18e4a33404607780ff95b1f735ea9c1b65fa0440 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 25 Aug 2016 13:45:13 -0400 Subject: [PATCH 19/21] update test to check org_auditor access --- awx/main/tests/functional/test_rbac_credential.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/tests/functional/test_rbac_credential.py b/awx/main/tests/functional/test_rbac_credential.py index 29c50f73e7..ae68f036d8 100644 --- a/awx/main/tests/functional/test_rbac_credential.py +++ b/awx/main/tests/functional/test_rbac_credential.py @@ -83,9 +83,10 @@ def test_credential_migration_org_auditor(credential, team, org_auditor): assert org_auditor not in credential.read_role rbac.migrate_credential(apps, None) + rbac.infer_credential_org_from_team(apps, None) # Read permissions post migration - assert org_auditor in credential.use_role + assert org_auditor not in credential.use_role assert org_auditor in credential.read_role def test_credential_access_superuser(): From baa2481944df267704f37b8b6a228b363c2906e6 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 26 Aug 2016 11:49:07 -0400 Subject: [PATCH 20/21] fixed localed date stuff --- .../client/lib/angular-scheduler/lib/angular-scheduler.js | 2 +- .../management-jobs/scheduler/schedulerForm.partial.html | 6 ------ awx/ui/client/src/scheduler/schedulerForm.partial.html | 6 ------ 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js b/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js index 437dabe341..6ef130f553 100644 --- a/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js +++ b/awx/ui/client/lib/angular-scheduler/lib/angular-scheduler.js @@ -34,7 +34,7 @@ if(input === null){ return ""; }else { - date = moment(input); + date = moment(input.split(" ")[0]); return date.format('l') + input.slice(input.indexOf(" ")); } }; diff --git a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html index 0592a0cfeb..b619ead47a 100644 --- a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html @@ -38,9 +38,6 @@
@@ -487,9 +484,6 @@
diff --git a/awx/ui/client/src/scheduler/schedulerForm.partial.html b/awx/ui/client/src/scheduler/schedulerForm.partial.html index e3fffecec7..fb13937b64 100644 --- a/awx/ui/client/src/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/scheduler/schedulerForm.partial.html @@ -38,9 +38,6 @@
@@ -469,9 +466,6 @@
From fd9d05d5df2b85e78fd3ed6749f2f2ef545d200e Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 30 Aug 2016 14:21:16 -0400 Subject: [PATCH 21/21] fixing deprecated_team.organization credential migration --- awx/main/fields.py | 8 ++------ awx/main/migrations/_rbac.py | 9 ++++++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 92ed69672f..e95dbc1ee7 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -54,10 +54,6 @@ class AutoOneToOneField(models.OneToOneField): AutoSingleRelatedObjectDescriptor(related)) - - - - def resolve_role_field(obj, field): ret = [] @@ -71,8 +67,8 @@ def resolve_role_field(obj, field): return [] if len(field_components) == 1: - Role_ = get_current_apps().get_model('main', 'Role') - if type(obj) is not Role_: + role_cls = str(get_current_apps().get_model('main', 'Role')) + if not str(type(obj)) == role_cls: raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj)))) ret.append(obj.id) else: diff --git a/awx/main/migrations/_rbac.py b/awx/main/migrations/_rbac.py index 245adc58ef..80ecc69ebc 100644 --- a/awx/main/migrations/_rbac.py +++ b/awx/main/migrations/_rbac.py @@ -2,7 +2,9 @@ import logging from time import time from django.utils.encoding import smart_text +from django.db import transaction from django.db.models import Q +from django.db.utils import IntegrityError from collections import defaultdict from awx.main.utils import getattrd @@ -489,7 +491,12 @@ def rebuild_role_hierarchy(apps, schema_editor): logger.info('Rebuild completed in %f seconds' % (stop - start)) logger.info('Done.') + def infer_credential_org_from_team(apps, schema_editor): Credential = apps.get_model('main', "Credential") for cred in Credential.objects.exclude(deprecated_team__isnull=True): - _update_credential_parents(cred.deprecated_team.organization, cred) + try: + with transaction.atomic(): + _update_credential_parents(cred.deprecated_team.organization, cred) + except IntegrityError: + logger.info("Organization<{}> credential for old Team<{}> credential already created".format(cred.deprecated_team.organization.pk, cred.pk))