From b16391604a50e105af67eff37b5a1165f8ef0c63 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Thu, 22 Apr 2021 13:30:30 -0500 Subject: [PATCH 1/9] Add suppport for hourly scanning --- .../static/recurrence/js/recurrence-widget.js | 1866 +++++++++++++++++ .../roles/console/tasks/main.yml | 9 + console/scan_scheduler.py | 115 +- 3 files changed, 1959 insertions(+), 31 deletions(-) create mode 100644 ansible-playbooks/roles/console/files/site-packages/recurrence/static/recurrence/js/recurrence-widget.js diff --git a/ansible-playbooks/roles/console/files/site-packages/recurrence/static/recurrence/js/recurrence-widget.js b/ansible-playbooks/roles/console/files/site-packages/recurrence/static/recurrence/js/recurrence-widget.js new file mode 100644 index 0000000..260cd1d --- /dev/null +++ b/ansible-playbooks/roles/console/files/site-packages/recurrence/static/recurrence/js/recurrence-widget.js @@ -0,0 +1,1866 @@ +if (!recurrence) + var recurrence = {}; + +recurrence.widget = {}; + + +recurrence.widget.Grid = function(cols, rows) { + this.init(cols, rows); +}; +recurrence.widget.Grid.prototype = { + init: function(cols, rows) { + this.disabled = false; + this.cells = []; + this.cols = cols; + this.rows = rows; + + this.init_dom(); + }, + + init_dom: function() { + var tbody = recurrence.widget.e('tbody'); + for (var y=0; y < this.rows; y++) { + var tr = recurrence.widget.e('tr'); + tbody.appendChild(tr); + for (var x=0; x < this.cols; x++) { + var td = recurrence.widget.e('td'); + tr.appendChild(td); + this.cells.push(td); + } + } + var table = recurrence.widget.e( + 'table', { + 'class': 'grid', 'cellpadding': 0, + 'cellspacing': 0, 'border': 0}, + [tbody]); + + this.elements = {'root': table, 'table': table, 'tbody': tbody}; + }, + + cell: function(col, row) { + return this.elements.tbody.childNodes[row].childNodes[col]; + }, + + enable: function () { + recurrence.widget.remove_class('disabled'); + this.disabled = false; + }, + + disable: function () { + recurrence.widget.add_class('disabled'); + this.disabled = true; + } +}; + + +recurrence.widget.Calendar = function(date, options) { + this.init(date, options); +}; +recurrence.widget.Calendar.prototype = { + init: function(date, options) { + this.date = date || recurrence.widget.date_today(); + this.month = this.date.getMonth(); + this.year = this.date.getFullYear(); + this.options = options || {}; + + if (this.options.onchange) + this.onchange = this.options.onchange; + if (this.options.onclose) + this.onclose = this.options.onclose; + + this.init_dom(); + this.show_month(this.year, this.month); + }, + + init_dom: function() { + var calendar = this; + + // navigation + + var remove = recurrence.widget.e('a', { + 'class': 'remove', + 'href': 'javascript:void(0)', + 'title': recurrence.display.labels.remove, + 'onclick': function() { + calendar.close(); + } + }, '×'); + var year_prev = recurrence.widget.e( + 'a', { + 'href': 'javascript:void(0)', 'class': 'prev-year', + 'onclick': function() {calendar.show_prev_year();}}, + '<<'); + var year_next = recurrence.widget.e( + 'a', { + 'href': 'javascript:void(0)', 'class': 'next-year', + 'onclick': function() {calendar.show_next_year();}}, + '>>'); + var month_prev = recurrence.widget.e( + 'a', { + 'href': 'javascript:void(0)', 'class': 'prev-month', + 'onclick': function() {calendar.show_prev_month();}}, + '<'); + var month_next = recurrence.widget.e( + 'a', { + 'href': 'javascript:void(0)', 'class': 'next-month', + 'onclick': function() {calendar.show_next_month();}}, + '>'); + var month_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.months[this.month]); + + var header_elements = [ + year_prev, month_prev, month_label, month_next, year_next]; + var header_grid = new recurrence.widget.Grid(header_elements.length, 1); + recurrence.array.foreach(header_elements, function(item, i) { + header_grid.cells[i].appendChild(item); + recurrence.widget.add_class( + header_grid.cells[i], item.className); + }); + recurrence.widget.add_class(header_grid.elements.root, 'navigation'); + + // core + + var calendar_year = recurrence.widget.e( + 'div', {'class': 'year'}, this.year); + var calendar_navigation = header_grid.elements.root; + // var calendar_week = week_grid.elements.root; + var calendar_body = recurrence.widget.e('div', {'class': 'body'}); + var calendar_footer = recurrence.widget.e('div', {'class': 'footer'}); + + var td = recurrence.widget.e( + 'td', {}, + [remove, calendar_year, calendar_navigation, + calendar_body, calendar_footer]); + var tr = recurrence.widget.e('tr', {}, [td]); + var tbody = recurrence.widget.e('tbody', {}, [tr]); + var root = recurrence.widget.e( + 'table', {'class': 'recurrence-calendar'}, [tbody]); + root.style.display = 'none'; + + this.elements = { + 'root': root, + 'year': calendar_year, + 'year_prev': year_prev, + 'year_next': year_next, + 'month_prev': month_prev, + 'month_next': month_next, + 'month_label': month_label, + 'calendar_body': calendar_body + }; + }, + + get_month_grid: function(year, month) { + var calendar = this; + + var dt = new Date(year, month, 1); + var start = dt.getDay(); + var days = recurrence.date.days_in_month(dt); + var rows = Math.ceil((days + start) / 7) + 1; + var grid = new recurrence.widget.Grid(7, rows); + + var number = 1; + recurrence.array.foreach( + grid.cells, function(cell, i) { + var cell = grid.cells[i]; + if (i < 7) { + var weekday_number = i - 1; + if (weekday_number < 0) + weekday_number = 6; + else if (weekday_number > 6) + weekday_number = 0; + cell.innerHTML = recurrence.display.weekdays_oneletter[ + weekday_number]; + recurrence.widget.add_class(cell, 'header'); + } else if (i - 7 < start || number > days) { + recurrence.widget.add_class(cell, 'empty'); + } else { + recurrence.widget.add_class(cell, 'day'); + if (this.date.getDate() == number && + this.date.getFullYear() == dt.getFullYear() && + this.date.getMonth() == dt.getMonth()) + recurrence.widget.add_class(cell, 'active'); + cell.innerHTML = number; + number = number + 1; + cell.onclick = function () { + calendar.set_date( + calendar.year, calendar.month, + parseInt(this.innerHTML, 10)); + }; + } + }, this); + + return grid; + }, + + show_month: function(year, month) { + if (this.elements.calendar_body.childNodes.length) + this.elements.calendar_body.removeChild( + this.elements.calendar_body.childNodes[0]); + this.elements.month_grid = this.get_month_grid(year, month); + this.elements.calendar_body.appendChild( + this.elements.month_grid.elements.root); + this.elements.month_label.firstChild.nodeValue = ( + recurrence.display.months[this.month]); + this.elements.year.firstChild.nodeValue = this.year; + }, + + show_prev_year: function() { + this.year = this.year - 1; + this.show_month(this.year, this.month); + }, + + show_next_year: function() { + this.year = this.year + 1; + this.show_month(this.year, this.month); + }, + + show_prev_month: function() { + this.month = this.month - 1; + if (this.month < 0) { + this.month = 11; + this.year = this.year - 1; + } + this.show_month(this.year, this.month); + }, + + show_next_month: function() { + this.month = this.month + 1; + if (this.month > 11) { + this.month = 0; + this.year = this.year + 1; + } + this.show_month(this.year, this.month); + }, + + set_date: function(year, month, day) { + if (year != this.date.getFullYear() || + month != this.date.getMonth() || + day != this.date.getDate()) { + + this.date.setFullYear(year); + this.date.setMonth(month); + this.date.setDate(day); + + recurrence.array.foreach( + this.elements.month_grid.cells, function(cell) { + if (recurrence.widget.has_class(cell, 'day')) { + var number = parseInt(cell.innerHTML, 10); + if (number == day) { + recurrence.widget.add_class(cell, 'active'); + } else { + recurrence.widget.remove_class(cell, 'active'); + } + } + }); + + if (this.onchange) + this.onchange(this.date); + } + }, + + set_position: function(x, y) { + this.elements.root.style.left = x + 'px'; + this.elements.root.style.top = y + 'px'; + }, + + show: function() { + this.elements.root.style.display = ''; + }, + + hide: function() { + this.elements.root.style.display = 'none'; + }, + + close: function() { + if (this.elements.root.parentNode) { + this.elements.root.parentNode.removeChild(this.elements.root); + if (this.onclose) + this.onclose(); + } + } +}; + + +recurrence.widget.DateSelector = function(date, options) { + this.init(date, options); +}; +recurrence.widget.DateSelector.prototype = { + init: function(date, options) { + this.disabled = false; + this.date = date; + this.calendar = null; + this.options = options || {}; + + if (this.options.onchange) + this.onchange = this.options.onchange; + + this.init_dom(); + }, + + init_dom: function() { + var dateselector = this; + + if (this.date) + var date_value = recurrence.date.format(this.date, '%Y-%m-%d'); + else + var date_value = ''; + var date_field = recurrence.widget.e( + 'input', { + 'class': 'date-field', 'size': 10, + 'value': date_value, + 'onchange': function() {dateselector.set_date(this.value);}}); + var calendar_button = recurrence.widget.e( + 'a', { + 'class': 'calendar-button', + 'href': 'javascript:void(0)', + 'title': recurrence.display.labels.calendar, + 'onclick': function() { + if (!dateselector.disabled) + dateselector.show_calendar(); + } + }, + '    '); + var root = recurrence.widget.e( + 'span', {'class': 'date-selector'}, + [date_field, calendar_button]); + + this.elements = { + 'root': root, + 'date_field': date_field, + 'calendar_button': calendar_button + }; + }, + + show_calendar: function() { + var dateselector = this; + + var calendar_blur = function(event) { + var element = event.target; + var is_in_dom = recurrence.widget.element_in_dom( + element, dateselector.calendar.elements.root); + if (!is_in_dom && + element != dateselector.elements.calendar_button) { + // clicked outside of calendar + dateselector.calendar.close(); + if (window.detachEvent) + window.detachEvent('onclick', calendar_blur); + else + window.removeEventListener('click', calendar_blur, false); + } + }; + + if (!this.calendar) { + this.calendar = new recurrence.widget.Calendar( + new Date((this.date || recurrence.widget.date_today()).valueOf()), { + 'onchange': function() { + dateselector.set_date( + recurrence.date.format(this.date, '%Y-%m-%d')); + dateselector.calendar.close(); + }, + 'onclose': function() { + if (window.detachEvent) + window.detachEvent('onclick', calendar_blur); + else + window.removeEventListener( + 'click', calendar_blur, false); + dateselector.hide_calendar(); + } + }); + document.body.appendChild(this.calendar.elements.root); + + this.calendar.show(); + this.set_calendar_position(); + + if (window.attachEvent) + window.attachEvent('onclick', calendar_blur); + else + window.addEventListener('click', calendar_blur, false); + } + }, + + set_date: function(datestring) { + var tokens = datestring.split('-'); + var year = parseInt(tokens[0], 10); + var month = parseInt(tokens[1], 10) - 1; + var day = parseInt(tokens[2], 10); + var dt = new Date(year, month, day); + + if (String(dt) == 'Invalid Date' || String(dt) == 'NaN') { + if (this.date && !this.options.allow_null) { + this.elements.date_field.value = recurrence.date.format( + this.date, '%Y-%m-%d'); + } else { + if (this.elements.date_field.value != '') { + if (this.onchange) + this.onchange(null); + } + this.elements.date_field.value = ''; + } + } else { + if (!this.date || + (year != this.date.getFullYear() || + month != this.date.getMonth() || + day != this.date.getDate())) { + + if (!this.date) + this.date = recurrence.widget.date_today(); + this.date.setFullYear(year); + this.date.setMonth(month); + this.date.setDate(day); + + this.elements.date_field.value = datestring; + + if (this.onchange) + this.onchange(this.date); + } + } + }, + + set_calendar_position: function() { + var loc = recurrence.widget.cumulative_offset( + this.elements.calendar_button); + + var calendar_x = loc[0]; + var calendar_y = loc[1]; + var calendar_right = ( + loc[0] + this.calendar.elements.root.clientWidth); + var calendar_bottom = ( + loc[1] + this.calendar.elements.root.clientHeight); + + if (calendar_right > document.scrollWidth) + calendar_x = calendar_x - ( + calendar_right - document.scrollWidth); + if (calendar_bottom > document.scrollHeight) + calendar_y = calendar_y - ( + calendar_bottom - document.scrollHeight); + + this.calendar.set_position(calendar_x, calendar_y); + }, + + hide_calendar: function() { + this.calendar = null; + }, + + enable: function () { + this.disabled = false; + this.elements.date_field.disabled = false; + }, + + disable: function () { + this.disabled = true; + this.elements.date_field.disabled = true; + if (this.calendar) + this.calendar.close(); + } +}; + + +recurrence.widget.Widget = function(textarea, options) { + this.init(textarea, options); +}; +recurrence.widget.Widget.prototype = { + init: function(textarea, options) { + if (textarea.toLowerCase) + textarea = document.getElementById(textarea); + this.selected_panel = null; + this.panels = []; + this.data = recurrence.deserialize(textarea.value); + this.textarea = textarea; + this.options = options; + + this.default_freq = options.default_freq || recurrence.WEEKLY; + + this.init_dom(); + this.init_panels(); + }, + + init_dom: function() { + var widget = this; + + var panels = recurrence.widget.e('div', {'class': 'panels'}); + var control = recurrence.widget.e('div', {'class': 'control'}); + var root = recurrence.widget.e( + 'div', {'class': this.textarea.className}, [panels, control]); + + var add_rule = new recurrence.widget.AddButton( + recurrence.display.labels.add_rule, { + 'onclick': function () {widget.add_rule();} + }); + recurrence.widget.add_class(add_rule.elements.root, 'add-rule'); + control.appendChild(add_rule.elements.root); + + var add_date = new recurrence.widget.AddButton( + recurrence.display.labels.add_date, { + 'onclick': function () {widget.add_date();} + }); + recurrence.widget.add_class(add_date.elements.root, 'add-date'); + control.appendChild(add_date.elements.root); + + this.elements = { + 'root': root, + 'panels': panels, + 'control': control + }; + + // attach immediately + this.textarea.style.display = 'none'; + this.textarea.parentNode.insertBefore( + this.elements.root, this.textarea); + }, + + init_panels: function() { + recurrence.array.foreach( + this.data.rrules, function(item) { + this.add_rule_panel(recurrence.widget.INCLUSION, item); + }, this); + recurrence.array.foreach( + this.data.exrules, function(item) { + this.add_rule_panel(recurrence.widget.EXCLUSION, item); + }, this); + recurrence.array.foreach( + this.data.rdates, function(item) { + this.add_date_panel(recurrence.widget.INCLUSION, item); + }, this); + recurrence.array.foreach( + this.data.exdates, function(item) { + this.add_date_panel(recurrence.widget.EXCLUSION, item); + }, this); + }, + + add_rule_panel: function(mode, rule) { + var panel = new recurrence.widget.Panel(this); + var form = new recurrence.widget.RuleForm(panel, mode, rule); + + panel.onexpand = function() { + if (panel.widget.selected_panel) + if (panel.widget.selected_panel != this) + panel.widget.selected_panel.collapse(); + panel.widget.selected_panel = this; + }; + panel.onremove = function() { + form.remove(); + }; + + this.elements.panels.appendChild(panel.elements.root); + this.panels.push(panel); + this.update(); + return panel; + }, + + add_date_panel: function(mode, date) { + var panel = new recurrence.widget.Panel(this); + var form = new recurrence.widget.DateForm(panel, mode, date); + + panel.onexpand = function() { + if (panel.widget.selected_panel) + if (panel.widget.selected_panel != this) + panel.widget.selected_panel.collapse(); + panel.widget.selected_panel = this; + }; + panel.onremove = function() { + form.remove(); + }; + + this.elements.panels.appendChild(panel.elements.root); + this.panels.push(panel); + this.update(); + return panel; + }, + + add_rule: function(rule) { + var rule = rule || new recurrence.Rule(this.default_freq); + this.data.rrules.push(rule); + this.add_rule_panel(recurrence.widget.INCLUSION, rule).expand(); + }, + + add_date: function(date) { + var date = date || recurrence.widget.date_today(); + this.data.rdates.push(date); + this.add_date_panel(recurrence.widget.INCLUSION, date).expand(); + }, + + update: function() { + this.textarea.value = this.data.serialize(); + } +}; + + +recurrence.widget.AddButton = function(label, options) { + this.init(label, options); +}; +recurrence.widget.AddButton.prototype = { + init: function(label, options) { + this.label = label; + this.options = options || {}; + + this.init_dom(); + }, + + init_dom: function() { + var addbutton = this; + + var plus = recurrence.widget.e( + 'span', {'class': 'plus'}, '+'); + var label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, this.label); + var root = recurrence.widget.e( + 'a', {'class': 'add-button', 'href': 'javascript:void(0)'}, + [plus, label]); + + root.onclick = function() { + addbutton.options.onclick(); + }; + + this.elements = {'root': root, 'plus': plus, 'label': label}; + } +}; + + +recurrence.widget.Panel = function(widget, options) { + this.init(widget, options); +}; +recurrence.widget.Panel.prototype = { + init: function(widget, options) { + this.collapsed = false; + this.widget = widget; + this.options = options || {}; + + if (this.options.onremove) + this.onremove = this.options.onremove; + if (this.options.onexpand) + this.onexpand = this.options.onexpand; + if (this.options.oncollapse) + this.oncollapse = this.options.oncollapse; + + this.init_dom(); + }, + + init_dom: function() { + var panel = this; + + var remove = recurrence.widget.e('a', { + 'class': 'remove', + 'href': 'javascript:void(0)', + 'title': recurrence.display.labels.remove, + 'onclick': function() { + panel.remove(); + } + }, '×'); + var label = recurrence.widget.e('a', { + 'class': 'recurrence-label', + 'href': 'javascript:void(0)', + 'onclick': function() { + if (panel.collapsed) + panel.expand(); + else + panel.collapse(); + } + }, ' '); + var header = recurrence.widget.e( + 'div', {'class': 'header'}, [remove, label]); + var body = recurrence.widget.e( + 'div', {'class': 'body'}); + var root = recurrence.widget.e( + 'div', {'class': 'panel'}, [header, body]); + + this.elements = { + 'root': root, 'remove': remove, 'label': label, + 'header': header, 'body': body + }; + + this.collapse(); + }, + + set_label: function(label) { + this.elements.label.innerHTML = label; + }, + + set_body: function(element) { + if (this.elements.body.childNodes.length) + this.elements.body.removeChild(this.elements.body.childNodes[0]); + this.elements.body.appendChild(element); + }, + + expand: function() { + this.collapsed = false; + this.elements.body.style.display = ''; + if (this.onexpand) + this.onexpand(this); + }, + + collapse: function() { + this.collapsed = true; + this.elements.body.style.display = 'none'; + if (this.oncollapse) + this.oncollapse(this); + }, + + remove: function() { + var parent = this.elements.root.parentNode; + if (parent) + parent.removeChild(this.elements.root); + if (this.onremove) + this.onremove(parent); + } +}; + + +recurrence.widget.RuleForm = function(panel, mode, rule, options) { + this.init(panel, mode, rule, options); +}; +recurrence.widget.RuleForm.prototype = { + init: function(panel, mode, rule, options) { + this.selected_freq = rule.freq; + this.panel = panel; + this.mode = mode; + this.rule = rule; + this.options = options || {}; + + var rule_options = { + interval: rule.interval, until: rule.until, count: rule.count + }; + + this.freq_rules = [ + new recurrence.Rule(recurrence.YEARLY, rule_options), + new recurrence.Rule(recurrence.MONTHLY, rule_options), + new recurrence.Rule(recurrence.WEEKLY, rule_options), + new recurrence.Rule(recurrence.DAILY, rule_options), + new recurrence.Rule(recurrence.HOURLY, rule_options) + ]; + this.freq_rules[this.rule.freq].update(this.rule); + + this.init_dom(); + + this.set_freq(this.selected_freq); + }, + + init_dom: function() { + var form = this; + + // mode + + var mode_checkbox = recurrence.widget.e( + 'input', {'class': 'checkbox', 'type': 'checkbox', 'name': 'mode'}); + var mode_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.exclude_occurrences); + var mode_container = recurrence.widget.e( + 'div', {'class': 'mode'}, + [mode_checkbox, mode_label]); + if (this.mode == recurrence.widget.EXCLUSION) + // delay for ie6 compatibility + setTimeout(function() { + mode_checkbox.checked = true; + recurrence.widget.add_class(form.panel, 'exclusion'); + }, 10); + + // freq + + var freq_choices = recurrence.display.frequencies.slice(0, 5); + var freq_options = recurrence.array.foreach( + freq_choices, function(item, i) { + var option = recurrence.widget.e( + 'option', {'value': i}, + recurrence.string.capitalize(item)); + return option; + }); + var freq_select = recurrence.widget.e( + 'select', {'name': 'freq'}, freq_options); + var freq_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.frequency + ':'); + var freq_container = recurrence.widget.e( + 'div', {'class': 'freq'}, + [freq_label, freq_select]); + + // interval + + var interval_field = recurrence.widget.e( + 'input', { + 'name': 'interval', 'size': 1, 'value': this.rule.interval}); + var interval_label1 = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.every); + var interval_label2 = recurrence.widget.e( + 'span', {'class': 'laebl'}, + recurrence.display.timeintervals_plural[this.rule.freq]); + var interval_container = recurrence.widget.e( + 'div', {'class': 'interval'}, + [interval_label1, interval_field, interval_label2]); + + // until + + if (this.rule.until) + until_value = recurrence.date.format(this.rule.until, '%Y-%m-%d'); + else + until_value = ''; + var until_radio = recurrence.widget.e( + 'input', {'class': 'radio', 'type': 'radio', + 'name': 'until_count', 'value': 'until'}); + var until_date_selector = new recurrence.widget.DateSelector( + this.rule.until, { + 'onchange': function(date) {form.set_until(date);}, + 'allow_null': true + }); + var until_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.date + ':'); + var until_container = recurrence.widget.e( + 'li', {'class': 'until'}, + [until_radio, until_label, until_date_selector.elements.root]); + + // count + + if (this.rule.count) + count_value = this.rule.count; + else + count_value = 1; + var count_radio = recurrence.widget.e( + 'input', { + 'class': 'radio', 'type': 'radio', + 'name': 'until_count', 'value': 'count'}); + var count_field = recurrence.widget.e( + 'input', {'name': 'count', 'size': 1, 'value': count_value}); + if (this.rule.count && this.rule.count < 2) + var token = recurrence.string.capitalize( + recurrence.display.labels.count); + else + var token = recurrence.string.capitalize( + recurrence.display.labels.count_plural); + var count_label1 = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, token.split('%(number)s')[0]); + var count_label2 = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, token.split('%(number)s')[1]); + var count_container = recurrence.widget.e( + 'li', {'class': 'count'}, + [count_radio, count_label1, count_field, count_label2]); + + // limit container + + var until_count_container = recurrence.widget.e( + 'ul', {'class': 'until-count'}, + [until_container, count_container]); + var limit_checkbox = recurrence.widget.e( + 'input', { + 'class': 'checkbox', 'type': 'checkbox', + 'name': 'limit'}); + var limit_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.repeat_until + ':'); + var limit_container = recurrence.widget.e( + 'div', {'class': 'limit'}, + [limit_checkbox, limit_label, until_count_container]); + if (this.rule.until || this.rule.count) { + // compatibility with ie, we delay + setTimeout(function() {limit_checkbox.checked = true;}, 10); + } else { + until_radio.disabled = true; + count_radio.disabled = true; + until_date_selector.disable(); + recurrence.widget.add_class(until_count_container, 'disabled'); + } + + // core + + var freq_form_container = recurrence.widget.e( + 'div', {'class': 'form'}); + var root = recurrence.widget.e( + 'form', {}, [ + mode_container, freq_container, interval_container, + freq_form_container, limit_container]); + + // events + + mode_checkbox.onclick = function() { + if (this.checked) + form.set_mode(recurrence.widget.EXCLUSION); + else + form.set_mode(recurrence.widget.INCLUSION); + }; + + freq_select.onchange = function() { + form.set_freq(parseInt(this.value), 10); + }; + + interval_field.onchange = function() { + form.set_interval(parseInt(this.value), 10); + }; + + limit_checkbox.onclick = function () { + if (this.checked) { + recurrence.widget.remove_class( + until_count_container, 'disabled'); + until_radio.disabled = false; + count_radio.disabled = false; + if (until_radio.checked) { + until_date_selector.enable(); + form.set_until(until_date_selector.date); + } + if (count_radio.checked) { + count_field.disabled = false; + form.set_count(parseInt(count_field.value)); + } + } else { + recurrence.widget.add_class( + until_count_container, 'disabled'); + until_radio.disabled = true; + count_radio.disabled = true; + until_date_selector.disable(); + count_field.disabled = true; + recurrence.array.foreach( + form.freq_rules, function(rule) { + rule.until = null; + rule.count = null; + }); + form.update(); + } + } + + // for compatibility with ie, use timeout + setTimeout(function () { + if (form.rule.count) { + count_radio.checked = true; + until_date_selector.disable(); + } else { + until_radio.checked = true; + count_field.disabled = true; + } + }, 1); + + until_radio.onclick = function () { + this.checked = true; + until_date_selector.enable(); + count_radio.checked = false; + count_field.disabled = true; + form.set_until(until_date_selector.date); + }; + + count_radio.onclick = function () { + this.checked = true; + count_field.disabled = false; + until_radio.checked = false; + until_date_selector.disable(); + form.set_count(parseInt(count_field.value), 10); + }; + + count_field.onchange = function () { + form.set_count(parseInt(this.value), 10); + }; + + // freq forms + + var forms = [ + recurrence.widget.RuleYearlyForm, + recurrence.widget.RuleMonthlyForm, + recurrence.widget.RuleWeeklyForm, + recurrence.widget.RuleDailyForm, + recurrence.widget.RuleHourlyForm + ]; + var freq_forms = recurrence.array.foreach( + forms, function(form, i) { + var rule = this.freq_rules[i]; + var f = new form(this, rule); + freq_form_container.appendChild(f.elements.root); + return f; + }, this); + + this.freq_forms = freq_forms; + + // install dom + + this.panel.set_label(this.get_display_text()); + this.panel.set_body(root); + + this.elements = { + 'root': root, + 'mode_checkbox': mode_checkbox, + 'freq_select': freq_select, + 'interval_field': interval_field, + 'freq_form_container': freq_form_container, + 'until_radio': until_radio, + 'count_field': count_field, + 'count_radio': count_radio, + 'limit_checkbox': limit_checkbox + }; + }, + + get_display_text: function() { + var text = this.freq_rules[this.selected_freq].get_display_text(); + if (this.mode == recurrence.widget.EXCLUSION) + text = recurrence.display.mode.exclusion + ' ' + text; + return recurrence.string.capitalize(text); + }, + + set_until: function(until) { + recurrence.array.foreach( + this.freq_rules, function(rule) { + rule.count = null; + rule.until = until; + }); + this.update(); + }, + + set_count: function(count) { + if (count < 2) + var token = recurrence.string.capitalize( + recurrence.display.labels.count); + else + var token = recurrence.string.capitalize( + recurrence.display.labels.count_plural); + var label1 = this.elements.count_field.previousSibling; + var label2 = this.elements.count_field.nextSibling; + label1.firstChild.nodeValue = token.split('%(number)s')[0]; + label2.firstChild.nodeValue = token.split('%(number)s')[1]; + recurrence.array.foreach( + this.freq_rules, function(rule) { + rule.until = null; + rule.count = count; + }); + this.update(); + }, + + set_interval: function(interval) { + interval = parseInt(interval, 10); + if (String(interval) == 'NaN') { + // invalid value, reset to previous value + this.elements.interval_field.value = ( + this.freq_rules[this.selected_freq].interval); + return; + } + + var label = this.elements.interval_field.nextSibling; + + if (interval < 2) + label.firstChild.nodeValue = ( + recurrence.display.timeintervals[this.selected_freq]); + else + label.firstChild.nodeValue = ( + recurrence.display.timeintervals_plural[this.selected_freq]); + recurrence.array.foreach( + this.freq_rules, function(rule) { + rule.interval = interval; + }); + + this.elements.interval_field.value = interval; + this.update(); + }, + + set_freq: function(freq) { + this.freq_forms[this.selected_freq].hide(); + this.freq_forms[freq].show(); + this.elements.freq_select.value = freq; + this.selected_freq = freq; + // need to update interval to display different label + this.set_interval(parseInt(this.elements.interval_field.value), 10); + this.update(); + }, + + set_mode: function(mode) { + if (this.mode != mode) { + if (this.mode == recurrence.widget.INCLUSION) { + recurrence.array.remove( + this.panel.widget.data.rrules, this.rule); + this.panel.widget.data.exrules.push(this.rule); + recurrence.widget.remove_class( + this.panel.elements.root, 'inclusion'); + recurrence.widget.add_class( + this.panel.elements.root, 'exclusion'); + } else { + recurrence.array.remove( + this.panel.widget.data.exrules, this.rule); + this.panel.widget.data.rrules.push(this.rule); + recurrence.widget.remove_class( + this.panel.elements.root, 'exclusion'); + recurrence.widget.add_class( + this.panel.elements.root, 'inclusion'); + } + this.mode = mode; + } + this.update(); + }, + + update: function() { + this.panel.set_label(this.get_display_text()); + this.rule.update(this.freq_rules[this.selected_freq]); + this.panel.widget.update(); + }, + + remove: function() { + var parent = this.elements.root.parentNode; + if (parent) + parent.removeChild(this.elements.root); + if (this.mode == recurrence.widget.INCLUSION) + recurrence.array.remove(this.panel.widget.data.rrules, this.rule); + else + recurrence.array.remove(this.panel.widget.data.exrules, this.rule); + this.panel.widget.update(); + } +}; + + +recurrence.widget.RuleYearlyForm = function(panel, rule) { + this.init(panel, rule); +}; +recurrence.widget.RuleYearlyForm.prototype = { + init: function(panel, rule) { + this.panel = panel; + this.rule = rule; + + this.init_dom(); + }, + + init_dom: function() { + var form = this; + + var grid = new recurrence.widget.Grid(4, 3); + var number = 0; + for (var y=0; y < 3; y++) { + for (var x=0; x < 4; x++) { + var cell = grid.cell(x, y); + if (this.rule.bymonth.indexOf(number + 1) > -1) + recurrence.widget.add_class(cell, 'active'); + cell.value = number + 1; + cell.innerHTML = recurrence.display.months_short[number]; + cell.onclick = function () { + if (recurrence.widget.has_class(this, 'active')) + recurrence.widget.remove_class(this, 'active'); + else + recurrence.widget.add_class(this, 'active'); + form.set_bymonth(); + }; + number += 1; + } + } + + // by weekday checkbox + + var byday_checkbox = recurrence.widget.e( + 'input', { + 'class': 'checkbox', 'type': 'checkbox', + 'name': 'byday'}); + var byday_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.string.capitalize( + recurrence.display.labels.on_the) + ':'); + var byday_container = recurrence.widget.e( + 'div', {'class': 'byday'}, + [byday_checkbox, byday_label]); + + // weekday-position + + var position_options = recurrence.array.foreach( + [1, 2, 3, -1, -2, -3], function(value) { + var option = recurrence.widget.e( + 'option', {'value': value}, + recurrence.string.strip(recurrence.display.weekdays_position[ + String(value)].split('%(weekday)s')[0])); + return option; + }); + var position_select = recurrence.widget.e( + 'select', {'name': 'position'}, position_options); + var weekday_options = recurrence.array.foreach( + recurrence.display.weekdays, function(weekday, i) { + var option = recurrence.widget.e( + 'option', {'value': i}, weekday); + return option; + }); + var weekday_select = recurrence.widget.e( + 'select', {'name': 'weekday'}, weekday_options); + var weekday_position_container = recurrence.widget.e( + 'div', {'class': 'section'}, [position_select, weekday_select]); + + // core + + var year = recurrence.widget.e('div'); + year.appendChild(grid.elements.root); + + var root = recurrence.widget.e( + 'div', {'class': 'yearly'}, + [year, byday_container, weekday_position_container]); + root.style.display = 'none'; + + if (this.rule.byday.length) { + if (form.rule.bysetpos.length) { + position_select.value = String(form.rule.bysetpos[0]); + } else { + position_select.value = String(form.rule.byday[0].index); + } + weekday_select.value = String(form.rule.byday[0].number); + byday_checkbox.checked = true; + } else { + position_select.disabled = true; + weekday_select.disabled = true; + } + + // events + + byday_checkbox.onclick = function () { + if (this.checked) { + position_select.disabled = false; + weekday_select.disabled = false; + form.set_byday(); + } else { + position_select.disabled = true; + weekday_select.disabled = true; + form.rule.byday = []; + form.panel.update(); + } + }; + + position_select.onchange = function () { + form.set_byday(); + }; + + weekday_select.onchange = function () { + form.set_byday(); + }; + + this.elements = { + 'root': root, + 'grid': grid, + 'byday_checkbox': byday_checkbox, + 'position_select': position_select, + 'weekday_select': weekday_select + }; + }, + + get_weekday: function() { + var number = parseInt(this.elements.weekday_select.value, 10); + var index = parseInt(this.elements.position_select.value, 10); + return new recurrence.Weekday(number, index); + }, + + set_bymonth: function() { + var bymonth = []; + recurrence.array.foreach( + this.elements.grid.cells, function(cell) { + if (recurrence.widget.has_class(cell, 'active')) + bymonth.push(cell.value); + }) + this.rule.bymonth = bymonth; + this.panel.update(); + }, + + set_byday: function() { + this.rule.byday = [this.get_weekday()]; + this.panel.update(); + }, + + show: function() { + this.elements.root.style.display = ''; + }, + + hide: function() { + this.elements.root.style.display = 'none'; + } +}; + + +recurrence.widget.RuleMonthlyForm = function(panel, rule) { + this.init(panel, rule); +}; +recurrence.widget.RuleMonthlyForm.prototype = { + init: function(panel, rule) { + this.panel = panel; + this.rule = rule; + + this.init_dom(); + }, + + init_dom: function() { + var form = this; + + // monthday + + var monthday_grid = new recurrence.widget.Grid(7, Math.ceil(31 / 7)); + var number = 0; + for (var y=0; y < Math.ceil(31 / 7); y++) { + for (var x=0; x < 7; x++) { + number += 1; + var cell = monthday_grid.cell(x, y); + if (number > 31) { + recurrence.widget.add_class(cell, 'empty'); + continue; + } else { + cell.innerHTML = number; + if (this.rule.bymonthday.indexOf(number) > -1) + recurrence.widget.add_class(cell, 'active'); + cell.onclick = function () { + if (monthday_grid.disabled) + return; + var day = parseInt(this.innerHTML, 10) || null; + if (day) { + if (recurrence.widget.has_class(this, 'active')) + recurrence.widget.remove_class(this, 'active'); + else + recurrence.widget.add_class(this, 'active'); + form.set_bymonthday(); + } + } + } + } + } + var monthday_grid_container = recurrence.widget.e( + 'div', {'class': 'section'}); + monthday_grid_container.appendChild(monthday_grid.elements.root); + var monthday_radio = recurrence.widget.e( + 'input', { + 'class': 'radio', 'type': 'radio', + 'name': 'monthly', 'value': 'monthday'}); + var monthday_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.each + ':'); + var monthday_container = recurrence.widget.e( + 'li', {'class': 'monthday'}, + [monthday_radio, monthday_label, monthday_grid_container]); + + // weekday-position + + var position_options = recurrence.array.foreach( + [1, 2, 3, -1, -2, -3], function(value) { + var option = recurrence.widget.e( + 'option', {'value': value}, + recurrence.string.strip( + recurrence.display.weekdays_position[ + String(value)].split('%(weekday)s')[0])); + return option; + }); + var position_select = recurrence.widget.e( + 'select', {'name': 'position'}, position_options); + + var weekday_options = recurrence.array.foreach( + recurrence.display.weekdays, function(weekday, i) { + var option = recurrence.widget.e( + 'option', {'value': i}, weekday); + return option; + }); + var weekday_select = recurrence.widget.e( + 'select', {'name': 'weekday'}, weekday_options); + var weekday_position_container = recurrence.widget.e( + 'div', {'class': 'section'}, [position_select, weekday_select]); + var weekday_radio = recurrence.widget.e( + 'input', { + 'class': 'radio', 'type': 'radio', + 'name': 'monthly', 'value': 'weekday'}); + var weekday_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.on_the + ':'); + var weekday_container = recurrence.widget.e( + 'li', {'class': 'weekday'}, + [weekday_radio, weekday_label, weekday_position_container]); + + // core + + var monthday_weekday_container = recurrence.widget.e( + 'ul', {'class': 'monthly'}, + [monthday_container, weekday_container]); + + var root = recurrence.widget.e( + 'div', {'class': 'monthly'}, [monthday_weekday_container]); + root.style.display = 'none'; + + // events + + // for compatibility with ie, use timeout + setTimeout(function () { + if (form.rule.byday.length) { + weekday_radio.checked = true; + if (form.rule.bysetpos.length) { + position_select.value = String(form.rule.bysetpos[0]); + } else { + position_select.value = String(form.rule.byday[0].index); + } + weekday_select.value = String(form.rule.byday[0].number); + monthday_grid.disable(); + } else { + monthday_radio.checked = true; + position_select.disabled = true; + weekday_select.disabled = true; + } + }, 1); + + monthday_radio.onclick = function () { + this.checked = true; + weekday_radio.checked = false; + position_select.disabled = true; + weekday_select.disabled = true; + monthday_grid.enable(); + form.set_bymonthday(); + }; + + weekday_radio.onclick = function () { + this.checked = true; + monthday_radio.checked = false; + position_select.disabled = false; + weekday_select.disabled = false; + monthday_grid.disable(); + form.set_byday(); + }; + + position_select.onchange = function () { + form.set_byday(); + }; + + weekday_select.onchange = function () { + form.set_byday(); + }; + + this.elements = { + 'root': root, + 'monthday_grid': monthday_grid, + 'monthday_radio': monthday_radio, + 'weekday_radio': weekday_radio, + 'position_select': position_select, + 'weekday_select': weekday_select + }; + }, + + get_weekday: function() { + var number = parseInt(this.elements.weekday_select.value, 10); + var index = parseInt(this.elements.position_select.value, 10); + return new recurrence.Weekday(number, index); + }, + + set_byday: function() { + this.rule.bymonthday = []; + this.rule.bysetpos = []; + this.rule.byday = [this.get_weekday()]; + this.panel.update(); + }, + + set_bymonthday: function() { + this.rule.bysetpos = []; + this.rule.byday = []; + var monthdays = []; + recurrence.array.foreach( + this.elements.monthday_grid.cells, function(cell) { + var day = parseInt(cell.innerHTML, 10) || null; + if (day && recurrence.widget.has_class(cell, 'active')) + monthdays.push(day); + }); + this.rule.bymonthday = monthdays; + this.panel.update(); + }, + + show: function() { + this.elements.root.style.display = ''; + }, + + hide: function() { + this.elements.root.style.display = 'none'; + } +}; + + +recurrence.widget.RuleWeeklyForm = function(panel, rule) { + this.init(panel, rule); +}; +recurrence.widget.RuleWeeklyForm.prototype = { + init: function(panel, rule) { + this.panel = panel; + this.rule = rule; + + this.init_dom(); + }, + + init_dom: function() { + var form = this; + + var weekday_grid = new recurrence.widget.Grid(7, 1); + var days = []; + var days = recurrence.array.foreach( + this.rule.byday, function(day) { + return recurrence.to_weekday(day).number; + }); + for (var x=0; x < 7; x++) { + var cell = weekday_grid.cell(x, 0); + if (days.indexOf(x) > -1) + recurrence.widget.add_class(cell, 'active'); + cell.value = x; + cell.innerHTML = recurrence.display.weekdays_short[x]; + cell.onclick = function () { + if (weekday_grid.disabled) + return; + if (recurrence.widget.has_class(this, 'active')) + recurrence.widget.remove_class(this, 'active'); + else + recurrence.widget.add_class(this, 'active'); + form.set_byday(); + }; + } + + var weekday_container = recurrence.widget.e( + 'div', {'class': 'section'}); + weekday_container.appendChild(weekday_grid.elements.root); + var root = recurrence.widget.e( + 'div', {'class': 'weekly'}, [weekday_container]); + root.style.display = 'none'; + + this.elements = { + 'root': root, + 'weekday_grid': weekday_grid + }; + }, + + set_byday: function() { + var byday = []; + recurrence.array.foreach( + this.elements.weekday_grid.cells, function(cell) { + if (recurrence.widget.has_class(cell, 'active')) + byday.push(new recurrence.Weekday(cell.value)); + }); + this.rule.byday = byday; + this.panel.update(); + }, + + show: function() { + this.elements.root.style.display = ''; + }, + + hide: function() { + this.elements.root.style.display = 'none'; + } +}; + + +recurrence.widget.RuleDailyForm = function(panel, rule) { + this.init(panel, rule); +}; +recurrence.widget.RuleDailyForm.prototype = { + init: function(panel, rule) { + this.panel = panel; + this.rule = rule; + + this.init_dom(); + }, + + init_dom: function() { + var root = recurrence.widget.e('div', {'class': 'daily'}); + root.style.display = 'none'; + this.elements = {'root': root}; + }, + + show: function() { + // this.elements.root.style.display = ''; + }, + + hide: function() { + // this.elements.root.style.display = 'none'; + } +}; + + +recurrence.widget.RuleHourlyForm = function(panel, rule) { + this.init(panel, rule); +}; +recurrence.widget.RuleHourlyForm.prototype = { + init: function(panel, rule) { + this.panel = panel; + this.rule = rule; + + this.init_dom(); + }, + + init_dom: function() { + var form = this; + + var grid = new recurrence.widget.Grid(8, 3); + + // var work_hours = [9, 10, 11, 12, 13, 14, 15, 16, 17]; + var work_hours = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]; + var number = -1; + for (var y=0; y < 3; y++) { + for (var x=0; x < 8; x++) { + number += 1; + var cell = grid.cell(x, y); + + cell.value = number; + cell.innerHTML = number; + if (work_hours.indexOf(number) !== -1) + recurrence.widget.add_class(cell, 'active'); + cell.onclick = function () { + var hour = parseInt(this.innerHTML, 10) || null; + if (recurrence.widget.has_class(this, 'active')) + recurrence.widget.remove_class(this, 'active'); + else + recurrence.widget.add_class(this, 'active'); + form.set_fromhour(); + } + } + } + + var hourly_container = recurrence.widget.e( + 'div', {'class': 'section'}); + hourly_container.appendChild(grid.elements.root); + + var root = recurrence.widget.e('div', {'class': 'hourly'}, [hourly_container]); + root.style.display = 'none'; + this.elements = {'root': root, 'grid': grid}; + form.set_fromhour(); + }, + + set_fromhour: function() { + var by_hour = []; + recurrence.array.foreach( + this.elements.grid.cells, function(cell) { + if (recurrence.widget.has_class(cell, 'active')) + by_hour.push(cell.value); + }); + this.rule.byhour = by_hour; + this.panel.update(); + }, + + show: function() { + this.elements.root.style.display = ''; + }, + + hide: function() { + this.elements.root.style.display = 'none'; + } +}; + +recurrence.widget.DateForm = function(panel, mode, date) { + this.init(panel, mode, date); +}; +recurrence.widget.DateForm.prototype = { + init: function(panel, mode, date) { + this.collapsed = true; + this.panel = panel; + this.mode = mode; + this.date = date; + + this.init_dom(); + }, + + init_dom: function() { + var form = this; + + // mode + + var mode_checkbox = recurrence.widget.e( + 'input', { + 'class': 'checkbox', 'type': 'checkbox', 'name': 'mode', + 'onclick': function() { + if (this.checked) + form.set_mode(recurrence.widget.EXCLUSION); + else + form.set_mode(recurrence.widget.INCLUSION); + } + }); + if (this.mode == recurrence.widget.EXCLUSION) + mode_checkbox.checked = true; + var mode_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, + recurrence.display.labels.exclude_date); + var mode_container = recurrence.widget.e( + 'div', {'class': 'mode'}, [mode_checkbox, mode_label]); + + // date + + var date_label = recurrence.widget.e( + 'span', {'class': 'recurrence-label'}, recurrence.display.labels.date + ':'); + var date_selector = new recurrence.widget.DateSelector( + this.date, {'onchange': function() {form.update();}}); + var date_container = recurrence.widget.e( + 'div', {'class': 'date'}, [date_label, date_selector.elements.root]); + + // core + + var root = recurrence.widget.e( + 'form', {'class': 'date'}, [mode_container, date_container]); + + // init dom + + this.panel.set_label(this.get_display_text()); + this.panel.set_body(root); + this.elements = {'root': root}; + }, + + get_display_text: function() { + var text = recurrence.date.format(this.date, '%l, %F %j, %Y'); + if (this.mode == recurrence.widget.EXCLUSION) + text = recurrence.display.mode.exclusion + ' ' + text; + return recurrence.string.capitalize(text); + }, + + set_mode: function(mode) { + if (this.mode != mode) { + if (this.mode == recurrence.widget.INCLUSION) { + recurrence.array.remove( + this.panel.widget.data.rdates, this.date); + this.panel.widget.data.exdates.push(this.date); + recurrence.widget.remove_class( + this.elements.root, 'inclusion'); + recurrence.widget.add_class( + this.elements.root, 'exclusion'); + this.update(); + } else { + recurrence.array.remove( + this.panel.widget.data.exdates, this.date); + this.panel.widget.data.rdates.push(this.date); + recurrence.widget.remove_class( + this.elements.root, 'exclusion'); + recurrence.widget.add_class( + this.elements.root, 'inclusion'); + this.update(); + } + this.mode = mode; + } + this.update(); + }, + + update: function() { + this.panel.set_label(this.get_display_text()); + this.panel.widget.update(); + }, + + remove: function() { + var parent = this.elements.root.parentNode; + if (parent) + parent.removeChild(this.elements.root); + if (this.mode == recurrence.widget.INCLUSION) + recurrence.array.remove(this.panel.widget.data.rdates, this.date); + else + recurrence.array.remove(this.panel.widget.data.exdates, this.date); + this.panel.widget.update(); + } +}; + + +recurrence.widget.e = function(tag_name, attrs, inner) { + var element = document.createElement(tag_name); + if (attrs) + recurrence.widget.set_attrs(element, attrs); + if (inner) { + if (!inner.toLowerCase && inner.length) + recurrence.array.foreach( + inner, function(e) {element.appendChild(e);}); + else + element.innerHTML = inner; + } + return element; +}; + + +recurrence.widget.set_attrs = function(element, attrs) { + for (var attname in attrs) + if (attname.match(/^on/g)) + element[attname] = attrs[attname]; + else if (attname == 'class') + element.className = attrs[attname]; + else + element.setAttribute(attname, attrs[attname]); +}; + + +recurrence.widget.add_class = function(element, class_name) { + var names = (element.className || '').split(/[ \r\n\t]+/g); + if (names.indexOf(class_name) == -1) { + names.push(class_name); + element.className = names.join(' '); + } +}; + + +recurrence.widget.remove_class = function(element, class_name) { + var names = (element.className || '').split(/[ \r\n\t]+/g); + if (names.indexOf(class_name) > -1) { + recurrence.array.remove(names, class_name); + element.className = names.join(' '); + } +}; + + +recurrence.widget.has_class = function(element, class_name) { + var names = (element.className || '').split(/[ \r\n\t]+/g); + if (names.indexOf(class_name) > -1) + return true; + else + return false; +}; + + +recurrence.widget.element_in_dom = function(element, dom) { + if (element == dom) { + return true; + } else { + for (var i=0; i < dom.childNodes.length; i++) + if (recurrence.widget.element_in_dom(element, dom.childNodes[i])) + return true; + } + return false; +}; + + +recurrence.widget.cumulative_offset = function(element) { + var y = 0, x = 0; + do { + y += element.offsetTop || 0; + x += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [x, y]; +}; + + +recurrence.widget.textareas_to_widgets = function(token) { + var elements = []; + if (!token) + token = 'recurrence-widget'; + if (token.toLowerCase) { + var textareas = document.getElementsByTagName('textarea'); + recurrence.array.foreach( + textareas, function(textarea) { + if (recurrence.widget.has_class(textarea, token)) + elements.push(textarea); + }); + } + recurrence.array.foreach( + elements, function(e) { + new recurrence.widget.Widget(e, window[e.id] || {}); + }); +}; + + +recurrence.widget.date_today = function() { + var date = new Date(); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + return date; +}; + + +recurrence.widget.INCLUSION = true; +recurrence.widget.EXCLUSION = false; + + +// display + + +if (!recurrence.display) + recurrence.display = {}; + +recurrence.display.mode = { + 'inclusion': gettext('including'), 'exclusion': gettext('excluding') +}; + +recurrence.display.labels = { + 'frequency': gettext('Frequency'), + 'on_the': gettext('On the'), + 'each': gettext('Each'), + 'every': gettext('Every'), + 'until': gettext('Until'), + 'count': gettext('Occurs %(number)s time'), + 'count_plural': gettext('Occurs %(number)s times'), + 'date': gettext('Date'), + 'time': gettext('Time'), + 'repeat_until': gettext('Repeat until'), + 'exclude_occurrences': gettext('Exclude these occurences'), + 'exclude_date': gettext('Exclude this date'), + 'add_rule': gettext('Add rule'), + 'add_date': gettext('Add date'), + 'remove': gettext('Remove'), + 'calendar': gettext('Calendar') +}; diff --git a/ansible-playbooks/roles/console/tasks/main.yml b/ansible-playbooks/roles/console/tasks/main.yml index 6138927..dfaa8f8 100644 --- a/ansible-playbooks/roles/console/tasks/main.yml +++ b/ansible-playbooks/roles/console/tasks/main.yml @@ -119,6 +119,15 @@ line: ' var language_code = "en";' backup: yes +# https://github.com/django-recurrence/django-recurrence/issues/155#issuecomment-806844193 +- name: Update recurrence-widget.js to support hourly frequency. + copy: + src: files/site-packages/recurrence/static/recurrence/js/recurrence-widget.js + dest: "{{ venv_dir }}/lib/python3.6/site-packages/recurrence/static/recurrence/js/recurrence-widget.js" + owner: "{{ non_root_user }}" + group: "{{ non_root_user }}" + mode: 0644 + - name: Change ownership of "{{ scantron_dir }}" folder. file: path: "{{ scantron_dir }}" diff --git a/console/scan_scheduler.py b/console/scan_scheduler.py index b8ea902..eafeeb2 100644 --- a/console/scan_scheduler.py +++ b/console/scan_scheduler.py @@ -113,43 +113,95 @@ def main(): # Determine time variables to assist in filtering. now_datetime = datetime.datetime.now() - now_time = now_datetime.time() - now_time_hour = now_time.hour - now_time_minute = now_time.minute - - # Only filter on scans that should start at this time based off hour and minute, ignoring seconds. - # If minute is the time resolution, this script (wrapped with scan_scheduler.sh) must be executed every minute - # through cron. Also filter on scans that are enabled. We can't filter on occurrences using Django's filter() - # method; it will have to be checked using logic below. - scans = ( - django_connector.Scan.objects.filter(start_time__hour=now_time_hour) - .filter(start_time__minute=now_time_minute) - .filter(enable_scan=True) - ) + + # Filter on enabled scans first. We can't filter on occurrences using Django's .filter() method; it will have to + # be checked using logic below. Author's reason why .filter() can't be used: + # https://github.com/django-recurrence/django-recurrence/issues/91#issuecomment-286890133 + # scans = django_connector.Scan.objects.filter(enable_scan=True) + scans = django_connector.Scan.objects.filter(enable_scan=True).filter(start_time__minute=now_datetime.minute) + # scans = django_connector.Scan.objects.filter(id=11) if not scans: - ROOT_LOGGER.info(f"No scans scheduled to start at this time: {now_time:%H}:{now_time:%M}.") + # ROOT_LOGGER.info(f"No scans scheduled to start at this time: {now_time:%H}:{now_time:%M}.") + ROOT_LOGGER.info("No scans enabled at this time.") return - ROOT_LOGGER.info(f"Found {len(scans)} scans scheduled to start at {now_time:%H}:{now_time:%M}.") + # ROOT_LOGGER.info(f"Found {len(scans)} scans scheduled to start at {now_time:%H}:{now_time:%M}.") + ROOT_LOGGER.info(f"Found {len(scans)} scans enabled") + + # now_datetime = datetime.datetime(2021, 4, 21, 15, 37) - # Loop through each scan that is scheduled to start at this time. + # Loop through each scan to determine if it's supposed to be scheduled. for scan in scans: - # Convoluted way of determining if a scan occurrence is today. - # Have fun understanding the documentation for django-recurrence. - # https://django-recurrence.readthedocs.io/en/latest/usage/recurrence_field.html#getting-occurrences-between-two-dates - # https://github.com/django-recurrence/django-recurrence/issues/50 - beginning_of_today = now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) - end_of_today = now_datetime.replace(hour=23).replace(minute=59).replace(second=59).replace(microsecond=0) - scan_occurrence = scan.recurrences.between(beginning_of_today, end_of_today, inc=True) + """ + Have fun understanding the documentation for django-recurrence! This is a challenging library to work with + since the django-recurrence README states "The recurrence field only deals with recurrences not with specific + time information." That's why a separate Scan.start_time field is required. A recurrence object has a + granularity of a date, and does not include time, so some challenging logic is required to determine a one-off + scan (no recurring schedule) vs. a recurring scan (with a possible hourly frequency). When using + scan.recurrence.between(), the start and end values are python datetime objects with a date granularity, so time + is completely ignored. + + The author has stated "I don't actually use this library now - so my support here is mostly just merging fixes + where I am comfortable with them, and pushing releases to PyPI. If someone else wants to take over ownership, + I'd be more than happy to hand it over." + (https://github.com/django-recurrence/django-recurrence/issues/163#issuecomment-604111964) + + I've provided verbose comments to explain my reasoning, but every time I come back to this code and library, + it takes me a day to figure out what's going on. + """ + + # Single one-off scans don't have recurring rules, just rdates. If an rrules attribute does not exist, it is + # likely a one-off scan. + if not scan.recurrences.rrules: + # Extract the single date the scan is supposed to start. + # rdate: datetime.datetime(2021, 4, 21, 5, 0, tzinfo=) + rdate = scan.recurrences.rdates[0] + + # Create a datetime.time object from now_datetime. + # now_datetime_time: datetime.time(11, 57) + now_datetime_time = datetime.time(now_datetime.time().hour, now_datetime.time().minute) + + # Check that the scan day and times match. + # rdate.date() and now_datetime.date() are datetime.date objects. + # scan.start_time and now_datetime_time are datetime.time objects. + if (rdate.date() == now_datetime.date()) and (scan.start_time == now_datetime_time): + # Replace current hour and minute with 0 to begin the search at the beginning of the day. We are only + # trying to find multiples of a date in which the time is irrelevant. + dtstart_datetime = ( + now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) + ) + + # Pare down now_datetime to include just the date and zero out the time since next_scans is a list of + # date and time recurrences. + now_datetime_stripped = datetime.datetime.combine(now_datetime.date(), datetime.time(0, 0)) + + # Scan does not have a recurrence, but the scan start date and start time are not correct. + else: + continue - # If a scan is not supposed to occur today, then bail, otherwise extract the datetime. - if not scan_occurrence: - continue else: - scan_occurrence = scan_occurrence[0] - ROOT_LOGGER.info(f"Found scan_occurrence for today: {scan_occurrence}.") + # Replace current hour with 0 to begin the search at the beginning of the day. Replace minute with the + # scan's start minute to try and find "multiples" of the start minute. + dtstart_datetime = ( + now_datetime.replace(hour=0) + .replace(minute=scan.start_time.minute) + .replace(second=0) + .replace(microsecond=0) + ) + + # Pare down now_datetime to include just the date and time since next_scans is a list of date and time + # recurrences. + now_datetime_stripped = now_datetime.replace(second=0).replace(microsecond=0) + + # Retrieve list of the next 25 definitive start times based off the dtstart_datetime. For a single scan, the + # size of next_scans will be 1, since it only occurs on one specific date. + next_scans = scan.recurrences.occurrences(dtstart=dtstart_datetime)[0:25] + + # If a scan is not supposed to occur at the current datetime, then bail, otherwise extract the datetime. + if now_datetime_stripped not in next_scans: + continue # Let's extract the remaining variables from existing database relationships. Note that the Scan model has the # Site model as a foreign key, and in turn, the Site model has foreign keys for the Engine and ScanCommand @@ -166,6 +218,8 @@ def main(): # Site model. site_name = scan.site.site_name + ROOT_LOGGER.info(f"Found scan found for {site_name} at {scan_start_time}.") + # Generate timestamps ##################### @@ -173,8 +227,7 @@ def main(): # and a recurrence date, so we have to build a DateTimeField equivalent. # Build start_datetime based off Django's TIME_ZONE setting. # https://www.saltycrane.com/blog/2009/05/converting-time-zones-datetime-objects-python/#add-timezone-localize - start_datetime_tz_naive = datetime.datetime.combine(scan_occurrence.date(), scan_start_time) - start_datetime = pytz.timezone(settings.TIME_ZONE).localize(start_datetime_tz_naive) + start_datetime = pytz.timezone(settings.TIME_ZONE).localize(now_datetime) # Convert start_datetime datetime object to string for result_file_base_name. timestamp = datetime.datetime.strftime(start_datetime, "%Y%m%d_%H%M") @@ -363,7 +416,7 @@ def main(): schedule_scan(scan_dict) else: - ROOT_LOGGER.critical(f"No engine or engine pool found...exiting.") + ROOT_LOGGER.critical("No engine or engine pool found...exiting.") sys.exit(1) From 824013b30ff382fd83a31f66234c989f06f36c3a Mon Sep 17 00:00:00 2001 From: derpadoo Date: Mon, 3 May 2021 13:56:51 -0500 Subject: [PATCH 2/9] v2 for hourly scanning --- console/django_scantron/admin.py | 2 +- console/django_scantron/models.py | 36 ++++++-- console/scan_scheduler.py | 149 +++++++++++++++--------------- 3 files changed, 102 insertions(+), 85 deletions(-) diff --git a/console/django_scantron/admin.py b/console/django_scantron/admin.py index 1b7864e..9284e23 100644 --- a/console/django_scantron/admin.py +++ b/console/django_scantron/admin.py @@ -44,7 +44,7 @@ class ScanAdmin(admin.ModelAdmin): list_display = ("id", "site", "scan_name", "enable_scan", "start_time", "recurrences") - exclude = ("completed_time", "result_file_base_name") + exclude = ("completed_time", "result_file_base_name", "dtstart") class SiteAdmin(admin.ModelAdmin): diff --git a/console/django_scantron/models.py b/console/django_scantron/models.py index 14551c1..8cd8658 100644 --- a/console/django_scantron/models.py +++ b/console/django_scantron/models.py @@ -5,6 +5,7 @@ from django.core.validators import MinValueValidator, RegexValidator from django.db.models.signals import post_save from django.dispatch import receiver +from django.utils.timezone import localtime, now from recurrence.fields import RecurrenceField from rest_framework.authtoken.models import Token @@ -268,20 +269,35 @@ class Scan(models.Model): enable_scan = models.BooleanField(verbose_name="Enable scan?") start_time = models.TimeField(verbose_name="Scan start time") recurrences = RecurrenceField(include_dtstart=False, verbose_name="Recurrences") + dtstart = models.DateTimeField( + blank=True, + null=True, + verbose_name="dtstart is the seed datetime object for recurrences (automatically modifed)", + ) + + # dtstart is the seed datetime object when determining scan_scheduler.py's + # scan_occurrences = scan.recurrences.between(beginning_of_today, end_of_today, dtstart=dtstart, inc=True), + # dtstart is updated on every model save. Currently, both the date and time are updated for dtstart. Not sure if + # updating the date really matters. + def save(self, *args, **kwargs): + + # current_scan = Scan.objects.get(pk=self.pk) + # if self.start_time != current_scan.start_time: + # if self.recurrences != current_scan.recurrences: + + now_datetime = localtime(now()) + self.dtstart = ( + now_datetime.replace(hour=self.start_time.hour) + .replace(minute=self.start_time.minute) + .replace(second=0) + .replace(microsecond=0) + ) + + return super().save(*args, **kwargs) def __str__(self): return str(self.id) - # def get_text_rules_inclusion(self): - # schedule_scan = ScheduledScan.objects.get(id=self.id) - # text_rules_inclusion = [] - # - # for rule in schedule_scan.recurrences.rrules: - # text_rules_inclusion.append(rule.to_text()) - # - # print(text_rules_inclusion) - # return text_rules_inclusion - class Meta: verbose_name_plural = "Scans" diff --git a/console/scan_scheduler.py b/console/scan_scheduler.py index eafeeb2..664bbea 100644 --- a/console/scan_scheduler.py +++ b/console/scan_scheduler.py @@ -10,6 +10,7 @@ # Third party Python libraries. from django.conf import settings +from django.utils.timezone import localtime # Custom Python libraries. import django_connector @@ -111,96 +112,92 @@ def schedule_scan(scan_dict): def main(): - # Determine time variables to assist in filtering. + # Set current date and time variables. + + # datetime.datetime(2021, 5, 3, 10, 21, 53, 197844) now_datetime = datetime.datetime.now() - # Filter on enabled scans first. We can't filter on occurrences using Django's .filter() method; it will have to + # datetime.time(10, 21, 53, 197844 + now_time = now_datetime.time() + + # Filter on enabled scans only. We can't filter on occurrences using Django's .filter() method; it will have to # be checked using logic below. Author's reason why .filter() can't be used: # https://github.com/django-recurrence/django-recurrence/issues/91#issuecomment-286890133 - # scans = django_connector.Scan.objects.filter(enable_scan=True) - scans = django_connector.Scan.objects.filter(enable_scan=True).filter(start_time__minute=now_datetime.minute) - # scans = django_connector.Scan.objects.filter(id=11) + scans = django_connector.Scan.objects.filter(enable_scan=True) if not scans: - # ROOT_LOGGER.info(f"No scans scheduled to start at this time: {now_time:%H}:{now_time:%M}.") - ROOT_LOGGER.info("No scans enabled at this time.") + ROOT_LOGGER.info("No scans enabled") return - # ROOT_LOGGER.info(f"Found {len(scans)} scans scheduled to start at {now_time:%H}:{now_time:%M}.") - ROOT_LOGGER.info(f"Found {len(scans)} scans enabled") - - # now_datetime = datetime.datetime(2021, 4, 21, 15, 37) - - # Loop through each scan to determine if it's supposed to be scheduled. + # Loop through each scan to determine if it is supposed to be scheduled. for scan in scans: """ - Have fun understanding the documentation for django-recurrence! This is a challenging library to work with - since the django-recurrence README states "The recurrence field only deals with recurrences not with specific - time information." That's why a separate Scan.start_time field is required. A recurrence object has a - granularity of a date, and does not include time, so some challenging logic is required to determine a one-off - scan (no recurring schedule) vs. a recurring scan (with a possible hourly frequency). When using - scan.recurrence.between(), the start and end values are python datetime objects with a date granularity, so time - is completely ignored. + Have fun understanding the documentation for django-recurrence! + + https://django-recurrence.readthedocs.io/en/latest/ + + This is a challenging library to work with since the django-recurrence README states "The recurrence field only + deals with recurrences not with specific time information." That's why a separate Scan.start_time field is + required. A recurrence object has a granularity of a date, and does not include time, so some challenging logic + is required to determine a one-off scan (no recurring schedule) vs. a recurring scan (with a possible hourly + frequency). When using scan.recurrence.between(), the start and end values are python datetime objects with a + date granularity, so time is completely ignored. Thus, a dtstart seed datetime object for recurrences is used. The author has stated "I don't actually use this library now - so my support here is mostly just merging fixes where I am comfortable with them, and pushing releases to PyPI. If someone else wants to take over ownership, I'd be more than happy to hand it over." (https://github.com/django-recurrence/django-recurrence/issues/163#issuecomment-604111964) - I've provided verbose comments to explain my reasoning, but every time I come back to this code and library, - it takes me a day to figure out what's going on. + I've tried to provide verbose comments to explain my reasoning, but every time I come back to this code and + library, it takes me a day to figure out what's going on. """ - # Single one-off scans don't have recurring rules, just rdates. If an rrules attribute does not exist, it is - # likely a one-off scan. - if not scan.recurrences.rrules: - # Extract the single date the scan is supposed to start. - # rdate: datetime.datetime(2021, 4, 21, 5, 0, tzinfo=) - rdate = scan.recurrences.rdates[0] - - # Create a datetime.time object from now_datetime. - # now_datetime_time: datetime.time(11, 57) - now_datetime_time = datetime.time(now_datetime.time().hour, now_datetime.time().minute) - - # Check that the scan day and times match. - # rdate.date() and now_datetime.date() are datetime.date objects. - # scan.start_time and now_datetime_time are datetime.time objects. - if (rdate.date() == now_datetime.date()) and (scan.start_time == now_datetime_time): - # Replace current hour and minute with 0 to begin the search at the beginning of the day. We are only - # trying to find multiples of a date in which the time is irrelevant. - dtstart_datetime = ( - now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) - ) + # datetime.datetime(2021, 5, 1, 0, 0) + beginning_of_today = now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) - # Pare down now_datetime to include just the date and zero out the time since next_scans is a list of - # date and time recurrences. - now_datetime_stripped = datetime.datetime.combine(now_datetime.date(), datetime.time(0, 0)) + # datetime.datetime(2021, 5, 3, 23, 59, 59) + end_of_today = now_datetime.replace(hour=23).replace(minute=59).replace(second=59).replace(microsecond=0) - # Scan does not have a recurrence, but the scan start date and start time are not correct. - else: - continue + # dtstart is time zone aware since it's coming from Django. Strip out the tzinfo to make it usable with both + # beginning_of_today and end_of_today. + # datetime.datetime(2021, 5, 3, 15, 24, tzinfo=) + dtstart = localtime(scan.dtstart).replace(tzinfo=None) + + # Retrieve all ths scan occurrences. + scan_occurrences = scan.recurrences.between(beginning_of_today, end_of_today, dtstart=dtstart, inc=True) + + # If no scan occurrences exist given the datetime parameters, move on to the next potential scan. + if not scan_occurrences: + continue + + # Pare down now_datetime (datetime.datetime(2021, 5, 3, 10, 21, 53, 197844)) to include just the date and time + # datetime.datetime(2021, 5, 3, 10, 21) + now_datetime_stripped = now_datetime.replace(second=0).replace(microsecond=0) + + # Further pare down the datetime object to just include a date and no time datetime.datetime(2021, 5, 3, 0, 0), + # for single one-off scans. In these cases, there isn't a recurrence since it is a one-time event. + now_datetime_stripped_only_date = now_datetime_stripped.replace(hour=0).replace(minute=0) + # datetime.time(10, 21) + now_time_stripped = now_time.replace(second=0).replace(microsecond=0) + + # Scans with an occurrence. + if now_datetime_stripped in scan_occurrences: + schedule_this_scan = True + + # Single one-off scans with a start time that matches the current time and date in scan_occurrences. + elif (scan.start_time.replace(second=0) == now_time_stripped) and ( + now_datetime_stripped_only_date in scan_occurrences + ): + schedule_this_scan = True + + # Scan scheduling criteria wasn't met. else: - # Replace current hour with 0 to begin the search at the beginning of the day. Replace minute with the - # scan's start minute to try and find "multiples" of the start minute. - dtstart_datetime = ( - now_datetime.replace(hour=0) - .replace(minute=scan.start_time.minute) - .replace(second=0) - .replace(microsecond=0) - ) - - # Pare down now_datetime to include just the date and time since next_scans is a list of date and time - # recurrences. - now_datetime_stripped = now_datetime.replace(second=0).replace(microsecond=0) - - # Retrieve list of the next 25 definitive start times based off the dtstart_datetime. For a single scan, the - # size of next_scans will be 1, since it only occurs on one specific date. - next_scans = scan.recurrences.occurrences(dtstart=dtstart_datetime)[0:25] - - # If a scan is not supposed to occur at the current datetime, then bail, otherwise extract the datetime. - if now_datetime_stripped not in next_scans: + schedule_this_scan = False + + # If the scheduled_scan bit was not set to True, move on. + if not schedule_this_scan: continue # Let's extract the remaining variables from existing database relationships. Note that the Scan model has the @@ -209,16 +206,18 @@ def main(): # ScanCommand models is updated, it will update the Site model, and cascade to the Scan model. # Scan model. - scan_start_time = scan.start_time - - # ScanCommand model. - scan_command = scan.site.scan_command.scan_command - scan_binary = scan.site.scan_command.scan_binary + # For the current scan_start_time, use now_time_stripped instead of scan.start_time in case an hourly recurrence + # frequency is used. + scan_start_time = now_time_stripped # Site model. site_name = scan.site.site_name - ROOT_LOGGER.info(f"Found scan found for {site_name} at {scan_start_time}.") + ROOT_LOGGER.info(f"Found scan for {site_name} at {scan_start_time}.") + + # ScanCommand model. + scan_command = scan.site.scan_command.scan_command + scan_binary = scan.site.scan_command.scan_binary # Generate timestamps ##################### @@ -434,6 +433,8 @@ def main(): console_handler.setFormatter(LOG_FORMATTER) ROOT_LOGGER.addHandler(console_handler) + ROOT_LOGGER.info("scan_scheduler.py started") + main() - ROOT_LOGGER.info("Done!") + ROOT_LOGGER.info("scan_scheduler.py completed") From 2abe1fab66683aa7a7b9f9da62b17a62deca978d Mon Sep 17 00:00:00 2001 From: derpadoo Date: Sat, 8 May 2021 14:46:14 -0500 Subject: [PATCH 3/9] Updated comments --- console/django_scantron/models.py | 4 ++-- console/scan_scheduler.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/console/django_scantron/models.py b/console/django_scantron/models.py index 8cd8658..08e11e8 100644 --- a/console/django_scantron/models.py +++ b/console/django_scantron/models.py @@ -277,8 +277,8 @@ class Scan(models.Model): # dtstart is the seed datetime object when determining scan_scheduler.py's # scan_occurrences = scan.recurrences.between(beginning_of_today, end_of_today, dtstart=dtstart, inc=True), - # dtstart is updated on every model save. Currently, both the date and time are updated for dtstart. Not sure if - # updating the date really matters. + # dtstart is updated on every Scan model save. Currently, both the date and time are updated for dtstart. Not sure + # if updating the date really matters. def save(self, *args, **kwargs): # current_scan = Scan.objects.get(pk=self.pk) diff --git a/console/scan_scheduler.py b/console/scan_scheduler.py index 664bbea..c9fc03a 100644 --- a/console/scan_scheduler.py +++ b/console/scan_scheduler.py @@ -112,12 +112,12 @@ def schedule_scan(scan_dict): def main(): - # Set current date and time variables. + # Set current date and time variables. Example datetime objects are provided throughout. # datetime.datetime(2021, 5, 3, 10, 21, 53, 197844) now_datetime = datetime.datetime.now() - # datetime.time(10, 21, 53, 197844 + # datetime.time(10, 21, 53, 197844) now_time = now_datetime.time() # Filter on enabled scans only. We can't filter on occurrences using Django's .filter() method; it will have to @@ -129,7 +129,7 @@ def main(): ROOT_LOGGER.info("No scans enabled") return - # Loop through each scan to determine if it is supposed to be scheduled. + # Loop through each scan to determine if it needs to be scheduled. for scan in scans: """ @@ -141,7 +141,7 @@ def main(): deals with recurrences not with specific time information." That's why a separate Scan.start_time field is required. A recurrence object has a granularity of a date, and does not include time, so some challenging logic is required to determine a one-off scan (no recurring schedule) vs. a recurring scan (with a possible hourly - frequency). When using scan.recurrence.between(), the start and end values are python datetime objects with a + frequency). When using scan.recurrence.between(), the start and end values are Python datetime objects with a date granularity, so time is completely ignored. Thus, a dtstart seed datetime object for recurrences is used. The author has stated "I don't actually use this library now - so my support here is mostly just merging fixes @@ -149,11 +149,11 @@ def main(): I'd be more than happy to hand it over." (https://github.com/django-recurrence/django-recurrence/issues/163#issuecomment-604111964) - I've tried to provide verbose comments to explain my reasoning, but every time I come back to this code and - library, it takes me a day to figure out what's going on. + I've tried to provide verbose comments to explain my reasoning and logic, but every time I come back to this + code and library, it takes me a day to figure out what's going on. """ - # datetime.datetime(2021, 5, 1, 0, 0) + # datetime.datetime(2021, 5, 3, 0, 0) beginning_of_today = now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) # datetime.datetime(2021, 5, 3, 23, 59, 59) From 5298deeba68ab70b182b00494ff27415be0b7707 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Sat, 8 May 2021 14:50:18 -0500 Subject: [PATCH 4/9] Bumped version --- console/django_scantron/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/django_scantron/__init__.py b/console/django_scantron/__init__.py index d0fbfe5..5094122 100644 --- a/console/django_scantron/__init__.py +++ b/console/django_scantron/__init__.py @@ -1 +1 @@ -__version__ = "1.43" +__version__ = "1.44" From 5c21037e44bd2deb41931bfa34ee67222826d8d7 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Tue, 11 May 2021 16:37:12 -0500 Subject: [PATCH 5/9] Another update to console/scan_scheduler.py --- console/scan_scheduler.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/console/scan_scheduler.py b/console/scan_scheduler.py index c9fc03a..8a70095 100644 --- a/console/scan_scheduler.py +++ b/console/scan_scheduler.py @@ -5,11 +5,9 @@ import ipaddress import itertools import logging -import pytz import sys # Third party Python libraries. -from django.conf import settings from django.utils.timezone import localtime # Custom Python libraries. @@ -114,8 +112,9 @@ def main(): # Set current date and time variables. Example datetime objects are provided throughout. - # datetime.datetime(2021, 5, 3, 10, 21, 53, 197844) - now_datetime = datetime.datetime.now() + # Use Django's app timezone to determine current datetime. + # datetime.datetime(2021, 5, 3, 10, 21, 53, 197844, tzinfo=) + now_datetime = localtime() # datetime.time(10, 21, 53, 197844) now_time = now_datetime.time() @@ -153,6 +152,12 @@ def main(): code and library, it takes me a day to figure out what's going on. """ + # Standardize the exdates. Just a note: https://github.com/django-recurrence/django-recurrence/issues/70 + for index, exdate in enumerate(scan.recurrences.exdates): + updated_exdate = localtime(exdate).replace(hour=now_time.hour).replace(minute=now_time.minute) + # print(f"Old exdate: {exdate} -- new exdate {updated_exdate}") + scan.recurrences.exdates[index] = updated_exdate + # datetime.datetime(2021, 5, 3, 0, 0) beginning_of_today = now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) @@ -161,8 +166,8 @@ def main(): # dtstart is time zone aware since it's coming from Django. Strip out the tzinfo to make it usable with both # beginning_of_today and end_of_today. - # datetime.datetime(2021, 5, 3, 15, 24, tzinfo=) - dtstart = localtime(scan.dtstart).replace(tzinfo=None) + # datetime.datetime(2021, 5, 3, 15, 24, tzinfo=) + dtstart = localtime(scan.dtstart) # Retrieve all ths scan occurrences. scan_occurrences = scan.recurrences.between(beginning_of_today, end_of_today, dtstart=dtstart, inc=True) @@ -222,11 +227,8 @@ def main(): # Generate timestamps ##################### - # start_datetime is a DateTimeField in ScheduledScan, but the Scan model only contains start_time (TimeField) - # and a recurrence date, so we have to build a DateTimeField equivalent. - # Build start_datetime based off Django's TIME_ZONE setting. - # https://www.saltycrane.com/blog/2009/05/converting-time-zones-datetime-objects-python/#add-timezone-localize - start_datetime = pytz.timezone(settings.TIME_ZONE).localize(now_datetime) + # Set start_datetime to now_datetime. + start_datetime = now_datetime # Convert start_datetime datetime object to string for result_file_base_name. timestamp = datetime.datetime.strftime(start_datetime, "%Y%m%d_%H%M") From 7da9eb3e976834f19cb108423eb61a81c30902b6 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Wed, 12 May 2021 16:15:06 -0500 Subject: [PATCH 6/9] Added timezone updates and API server/client updates --- README.md | 11 +-- ansible-playbooks/group_vars/all | 6 ++ ansible-playbooks/roles/common/tasks/main.yml | 4 +- ansible-playbooks/roles/common/vars/main.yml | 1 - .../roles/console/tasks/main.yml | 10 ++- .../roles/console/templates/local.py.j2 | 30 +++++++ .../roles/console/templates/production.py.j2 | 3 + console/config/settings/base.py | 5 +- console/django_scantron/api/urls.py | 1 + console/django_scantron/api/views.py | 23 ++--- console/scan_scheduler.py | 3 +- console/scan_scheduler_visualizer.py | 89 +++++++++++++++++++ scantron_api_client/scantron_api_client.py | 39 ++++++-- 13 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 ansible-playbooks/roles/console/templates/local.py.j2 create mode 100644 console/scan_scheduler_visualizer.py diff --git a/README.md b/README.md index 09aec0f..288787a 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,11 @@ The recommendation is to deploy the console first. #### Update Console Ansible Variables -Edit any variables in these files before running playbook: +Edit any variables in `ansible-playbooks/group_vars/all` before running playbook. Note the time zone variables: + +* `timezone_server` - Set this to be the timezone you want the server to be in, usually UTC. +* `timezone_django` - Set this to be your local timezone. It makes dealing with dates, times, and scheduling easier. -* `ansible-playbooks/group_vars/all` - If you plan on utilizing the same API key across all engines (not recommended, but easier for automated deployments), change `utilize_static_api_token_across_engines` to `True`. This prevents you from having to log into each engine and update `engine_config.json` with the corresponding API key. The `group_vars/static_api_key` will be created by the @@ -134,11 +135,11 @@ API key found in `group_vars/static_api_key`. more than 1 engine, you won't run into complications with engine name collisions. You will, however, need to add create the user on the console, since the console returns scheduled jobs to the engine based off the engine's name! +#### Update Console Secrets Variables + Rename `console/scantron_secrets.json.empty` to `console/scantron_secrets.json` (should be done for you by `initial_setup.sh`) -#### Update Console Secrets Variables - Update all the values `console/scantron_secrets.json` if you do not like ones generated using `initial_setup.sh`. Only the `production` values are used. diff --git a/ansible-playbooks/group_vars/all b/ansible-playbooks/group_vars/all index d8fccd9..d294e26 100644 --- a/ansible-playbooks/group_vars/all +++ b/ansible-playbooks/group_vars/all @@ -23,3 +23,9 @@ install_masscan_on_engine: True # instead of having to logging into every engine and updating engine/engine_config.json # This is less secure! utilize_static_api_token_across_engines: False + +# timezone used for the server's OS. +timezone_server: UTC + +# timezone used for the Django application's TIME_ZONE setting. Should be your local timezone. +timezone_django: America/Chicago diff --git a/ansible-playbooks/roles/common/tasks/main.yml b/ansible-playbooks/roles/common/tasks/main.yml index cde5b15..3cd4848 100644 --- a/ansible-playbooks/roles/common/tasks/main.yml +++ b/ansible-playbooks/roles/common/tasks/main.yml @@ -79,9 +79,9 @@ command: update-grub2 when: disable_ipv6 -- name: Set timezone. +- name: Set server timezone. timezone: - name: "{{ timezone }}" + name: "{{ timezone_server }}" - name: Reboot the box in 1 minute command: shutdown -r 1 diff --git a/ansible-playbooks/roles/common/vars/main.yml b/ansible-playbooks/roles/common/vars/main.yml index c5d5f4c..bee45c4 100644 --- a/ansible-playbooks/roles/common/vars/main.yml +++ b/ansible-playbooks/roles/common/vars/main.yml @@ -2,7 +2,6 @@ enable_ufw_firewall: true reboot_box: false disable_ipv6: false # breaks nginx install -timezone: UTC # apt packages install_packages: diff --git a/ansible-playbooks/roles/console/tasks/main.yml b/ansible-playbooks/roles/console/tasks/main.yml index dfaa8f8..b9f18f0 100644 --- a/ansible-playbooks/roles/console/tasks/main.yml +++ b/ansible-playbooks/roles/console/tasks/main.yml @@ -209,15 +209,17 @@ mode: 0644 tags: update_code -- name: Update production.py with local/production environment variable. +- name: Update local.py/production.py with Django configurations. template: - src: templates/production.py.j2 - dest: "{{ scantron_dir }}/config/settings/production.py" + src: templates/{{ item }}.py.j2 + dest: "{{ scantron_dir }}/config/settings/{{ item }}.py" backup: no owner: "{{ non_root_user }}" group: "{{ non_root_user }}" mode: 0644 - when: application_environment == "production" + with_items: + - local + - production tags: update_code - name: Update django_connector.py with local/production environment variable. diff --git a/ansible-playbooks/roles/console/templates/local.py.j2 b/ansible-playbooks/roles/console/templates/local.py.j2 new file mode 100644 index 0000000..f4aacb7 --- /dev/null +++ b/ansible-playbooks/roles/console/templates/local.py.j2 @@ -0,0 +1,30 @@ +""" +Local settings + +- Run in Debug mode +- Add Django Debug Toolbar +""" +from .base import * # noqa + + +# DEBUG +# ------------------------------------------------------------------------------ +# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug +DEBUG = True + +# django-debug-toolbar +# ------------------------------------------------------------------------------ +MIDDLEWARE += [ # noqa + "debug_toolbar.middleware.DebugToolbarMiddleware", +] +INSTALLED_APPS += [ # noqa + "debug_toolbar", +] + + +INTERNAL_IPS = ["127.0.0.1"] + +TIME_ZONE = "{{ timezone_django }}" + +# Your local stuff: Below this line define 3rd party library settings +# ------------------------------------------------------------------------------ diff --git a/ansible-playbooks/roles/console/templates/production.py.j2 b/ansible-playbooks/roles/console/templates/production.py.j2 index 20c7f51..74d5720 100644 --- a/ansible-playbooks/roles/console/templates/production.py.j2 +++ b/ansible-playbooks/roles/console/templates/production.py.j2 @@ -70,6 +70,9 @@ LOGGING = { }, }, } + +TIME_ZONE = "{{ timezone_django }}" + # fmt: on # Your production stuff: Below this line define 3rd party library settings # ------------------------------------------------------------------------------ diff --git a/console/config/settings/base.py b/console/config/settings/base.py index 1e586d7..fc30742 100644 --- a/console/config/settings/base.py +++ b/console/config/settings/base.py @@ -124,8 +124,9 @@ def get_secret(setting, secrets=SECRETS): # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. -# TIME_ZONE = 'UTC' -TIME_ZONE = "America/Chicago" +# Over-ridden by setting in scantron/ansible-playbooks/roles/console/templates/production.py.j2 and +# scantron/ansible-playbooks/roles/console/templates/local.py.j2 +TIME_ZONE = "UTC" # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code LANGUAGE_CODE = "en-us" diff --git a/console/django_scantron/api/urls.py b/console/django_scantron/api/urls.py index 93ae872..b3bac96 100644 --- a/console/django_scantron/api/urls.py +++ b/console/django_scantron/api/urls.py @@ -33,6 +33,7 @@ url(r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), name="schema-json"), url(r"^swagger/$", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), url(r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + url(r"^server_time$", views.get_server_time, name="server_time"), ] urlpatterns += router.urls diff --git a/console/django_scantron/api/views.py b/console/django_scantron/api/views.py index 18de28b..d898a82 100644 --- a/console/django_scantron/api/views.py +++ b/console/django_scantron/api/views.py @@ -1,13 +1,12 @@ # Standard Python libraries. -import datetime import os -import pytz # Third party Python libraries. -from django.conf import settings from django.http import Http404, JsonResponse +from django.utils.timezone import localtime import redis from rest_framework import mixins, viewsets +from rest_framework.decorators import api_view from rest_framework.permissions import IsAdminUser, IsAuthenticated import rq @@ -33,14 +32,16 @@ import utility -def get_current_time(): - """Retrieve a Django compliant pre-formated datetimestamp.""" +@api_view(["GET"]) +def get_server_time(request): + """Return the server time as a string according to Django's TIME_ZONE configuration setting.""" - datetime_tz_naive = datetime.datetime.now() - django_timezone = settings.TIME_ZONE - datetime_tz = pytz.timezone(django_timezone).localize(datetime_tz_naive) + # Returns a string like "2021-05-11T16:07:34.480844-05:00". + results_dict = { + "server_time": localtime().isoformat(), + } - return datetime_tz + return JsonResponse(results_dict) class ListRetrieveUpdateViewSet( @@ -183,7 +184,7 @@ def partial_update(self, request, pk=None, **kwargs): ) # Django compliant pre-formated datetimestamp. - now_datetime = get_current_time() + now_datetime = localtime() ScheduledScan.objects.filter(scan_engine=request.user).filter(pk=pk).update( completed_time=now_datetime ) @@ -217,7 +218,7 @@ def get_queryset(self): user = self.request.user # Django compliant pre-formated datetimestamp. - now_datetime = get_current_time() + now_datetime = localtime() # Update last_checkin time. Engine.objects.filter(scan_engine=user).update(last_checkin=now_datetime) diff --git a/console/scan_scheduler.py b/console/scan_scheduler.py index 8a70095..26174d3 100644 --- a/console/scan_scheduler.py +++ b/console/scan_scheduler.py @@ -164,8 +164,7 @@ def main(): # datetime.datetime(2021, 5, 3, 23, 59, 59) end_of_today = now_datetime.replace(hour=23).replace(minute=59).replace(second=59).replace(microsecond=0) - # dtstart is time zone aware since it's coming from Django. Strip out the tzinfo to make it usable with both - # beginning_of_today and end_of_today. + # dtstart is time zone aware since it's coming from Django. # datetime.datetime(2021, 5, 3, 15, 24, tzinfo=) dtstart = localtime(scan.dtstart) diff --git a/console/scan_scheduler_visualizer.py b/console/scan_scheduler_visualizer.py new file mode 100644 index 0000000..5acc7b3 --- /dev/null +++ b/console/scan_scheduler_visualizer.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +# Standard Python libraries. +import argparse +import datetime + +# Third party Python libraries. +from django.utils.timezone import localtime + +# Custom Python libraries. +import django_connector + + +def main(number_of_days_in_the_future=5, scan_id=None): + + # Set current date and time variables. Example datetime objects are provided throughout. + + # datetime.datetime(2021, 5, 3, 10, 21, 53, 197844, tzinfo=) + now_datetime = localtime() + + # datetime.time(10, 21, 53, 197844) + now_time = now_datetime.time() + + # Filter on specified scan ID or all enabled scans. + if scan_id: + scans = django_connector.Scan.objects.filter(id=scan_id) + else: + scans = django_connector.Scan.objects.filter(enable_scan=True) + + for scan in scans: + + # Standardize the exdates. Just a note: https://github.com/django-recurrence/django-recurrence/issues/70 + for index, exdate in enumerate(scan.recurrences.exdates): + updated_exdate = localtime(exdate).replace(hour=now_time.hour).replace(minute=now_time.minute) + print(f"Old exdate: {exdate} -- new exdate {updated_exdate}") + scan.recurrences.exdates[index] = updated_exdate + + # datetime.datetime(2021, 5, 3, 0, 0) + beginning_of_today = now_datetime.replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) + + # datetime.datetime(2021, 5, 3, 23, 59, 59) + future_end_datetime = beginning_of_today + datetime.timedelta(days=number_of_days_in_the_future) + + # dtstart is time zone aware since it's coming from Django. + # datetime.datetime(2021, 5, 3, 15, 24, tzinfo=) + dtstart = localtime(scan.dtstart) + + # Retrieve all ths scan occurrences. + scan_occurrences = scan.recurrences.between(beginning_of_today, future_end_datetime, dtstart=dtstart, inc=True) + + if scan_occurrences: + + print(f"Scan ID: {scan.id}") + print(f"Scan start time: {scan.start_time}") + print(f"{len(scan_occurrences)} total scans between {beginning_of_today} and {future_end_datetime}") + + for scan_occurrence in scan_occurrences: + print(f"\t{scan_occurrence}") + + if scan.recurrences.exdates: + print("exdates") + for exdate in scan.recurrences.exdates: + print(f"\t{exdate}") + + print("=" * 20) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description=( + "Visualize the scan start dates and times from the beginning of today to a specified number of days in the " + "future for enabled scans." + ) + ) + parser.add_argument( + "-d", + dest="number_of_days_in_the_future", + action="store", + type=int, + default=5, + help="Number of days in the future. Default 5.", + ) + parser.add_argument( + "-s", dest="scan_id", action="store", type=int, required=False, help="Specify a scan ID.", + ) + args = parser.parse_args() + + main(**vars(args)) diff --git a/scantron_api_client/scantron_api_client.py b/scantron_api_client/scantron_api_client.py index 1d4a860..c70acae 100644 --- a/scantron_api_client/scantron_api_client.py +++ b/scantron_api_client/scantron_api_client.py @@ -11,7 +11,7 @@ import utility -__version__ = "0.0.4" +__version__ = "0.0.5" class ScantronClient: @@ -199,6 +199,17 @@ def scantron_api_query(self, endpoint, **kwargs): return response + def retrieve_server_time(self): + """Retrieve the date and time on the server. Useful when scheduling scans using the API. Returns a string of + Django's localtime().isoformat.""" + + response = self.scantron_api_query(f"/api/server_time") + + if response.status_code == 200: + server_time = response.json()["server_time"] + + return server_time + # Scan Results ############## def retrieve_scan_results(self, scan_id, file_type, write_to_disk=False, **kwargs): @@ -210,7 +221,7 @@ def retrieve_scan_results(self, scan_id, file_type, write_to_disk=False, **kwarg file_type = file_type.lower() file_name = f"scan_results_{scan_id}.{file_type}" - if file_type not in ["nmap", "xml", "json"]: + if file_type not in ["nmap", "xml", "json", "pooled"]: print(f"Not a valid file type: {file_type}") else: @@ -223,7 +234,7 @@ def retrieve_scan_results(self, scan_id, file_type, write_to_disk=False, **kwarg with open(file_name, "w") as fh: fh.write(scan_results) - elif response.status_code == 200 and file_type == "json": + elif response.status_code == 200 and file_type in ["json", "pooled"]: try: scan_results = response.json() @@ -545,8 +556,9 @@ def retrieve_all_masscan_targets_with_an_open_port(self, masscan_dict): all_open_udp_ports_csv = ",".join(list(map(str, all_open_udp_ports_list))) all_targets_with_an_open_port_dict = { - "all_targets_with_an_open_port_list": all_targets_with_an_open_port, - "all_targets_with_an_open_port_csv": ",".join(all_targets_with_an_open_port), + "all_targets_with_an_open_port_as_list": all_targets_with_an_open_port, + "all_targets_with_an_open_port_as_csv": ",".join(all_targets_with_an_open_port), + "all_targets_with_an_open_port_as_spaced": " ".join(all_targets_with_an_open_port), "all_targets_with_an_open_port_size": len(all_targets_with_an_open_port), "all_open_tcp_ports_list": all_open_tcp_ports_list, "all_open_udp_ports_list": all_open_udp_ports_list, @@ -616,7 +628,7 @@ def retrieve_all_masscan_targets_with_a_specific_port_and_protocol(self, masscan return all_targets_with_a_specific_port_and_protocol_dict def retrieve_all_masscan_targets_with_a_specific_port_and_protocol_from_scan_id( - self, scan_id, port, protocol="tcp" + self, scan_id, port, protocol="tcp", file_type="json" ): """Retrieves all the targets with a specified open port and protocol given a scan ID. Only supports masscan .json files.""" @@ -630,11 +642,11 @@ def retrieve_all_masscan_targets_with_a_specific_port_and_protocol_from_scan_id( "all_targets_with_a_specific_port_and_protocol_spaced": "", } - scan_results_json = self.retrieve_scan_results(scan_id, "json") + scan_results_json = self.retrieve_scan_results(scan_id, file_type) masscan_dict = self.generate_masscan_dict_from_masscan_result(scan_results_json) - all_targets_with_a_specific_port_and_protocol_dict = ( - self.retrieve_all_masscan_targets_with_a_specific_port_and_protocol(masscan_dict, port, protocol) + all_targets_with_a_specific_port_and_protocol_dict = self.retrieve_all_masscan_targets_with_a_specific_port_and_protocol( + masscan_dict, port, protocol ) # Add scan ID to returned dictionary. @@ -642,6 +654,15 @@ def retrieve_all_masscan_targets_with_a_specific_port_and_protocol_from_scan_id( return all_targets_with_a_specific_port_and_protocol_dict + def retrieve_all_masscan_targets_and_open_ports_from_scan_id(self, scan_id, file_type="json"): + """Retrieves all the targets and open ports given a scan ID. Only supports masscan .json files.""" + + scan_results_json = self.retrieve_scan_results(scan_id, file_type) + masscan_dict = self.generate_masscan_dict_from_masscan_result(scan_results_json) + all_masscan_targets_and_open_ports = self.retrieve_all_masscan_targets_with_an_open_port(masscan_dict) + + return all_masscan_targets_and_open_ports + def wait_until_scheduled_scan_finishes(self, scheduled_scan_id, sleep_seconds=60): """Given a scheduled scan ID, sleep until the scan finishes.""" From 51d3839218328bc6919309f4a3b95a609ab14e86 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Thu, 13 May 2021 12:40:09 -0500 Subject: [PATCH 7/9] Added retrieve_next_available_scan_time() API client function --- scantron_api_client/README.md | 20 +++++++++++-- scantron_api_client/scantron_api_client.py | 34 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/scantron_api_client/README.md b/scantron_api_client/README.md index d69229b..e0da008 100644 --- a/scantron_api_client/README.md +++ b/scantron_api_client/README.md @@ -103,19 +103,33 @@ print(response.json()) The `recurrences` value can be tricky. If you are using a complicated one, open developer tools in your browser, create the scan through the web GUI, and inspect the POST request. Note there is a newline (`\n`) between `RRULE` and `RDATE`. -At a minimum, you need to include `RDATE:`. +`RRULE` isn't required, but at a minimum you need to include `RDATE:` for a single non-recurring scan. ![recurrences](recurrences.png) ```python + +# Option 1 - Scan at a future time. payload = { "site": 1, "scan_name": "DMZ Scan", "enable_scan": True, - "start_time": "16:00:00", - "recurrences": "RRULE:FREQ=WEEKLY;BYDAY=MO\nRDATE:20200113T060000Z", + "start_time": "16:00", + "recurrences": "RRULE:FREQ=WEEKLY;BYDAY=MO", } +# Option 2 - Schedule at the next available start time. +next_eligible_scan_string = sc.retrieve_next_available_scan_time() + +payload = { + "site": 1, + "scan_name": "DMZ Scan", + "enable_scan": True, + "start_time": next_eligible_scan_string, + "recurrences": "RRULE:FREQ=WEEKLY;BYDAY=MO", +} + + response = sc.create_scan(payload) print(response.status_code) print(response.json()) diff --git a/scantron_api_client/scantron_api_client.py b/scantron_api_client/scantron_api_client.py index c70acae..3d0d895 100644 --- a/scantron_api_client/scantron_api_client.py +++ b/scantron_api_client/scantron_api_client.py @@ -1,4 +1,5 @@ # Standard Python libraries. +import datetime import json import sys import time @@ -670,6 +671,39 @@ def wait_until_scheduled_scan_finishes(self, scheduled_scan_id, sleep_seconds=60 print(f"Scheduled scan ID {scheduled_scan_id} is still running...sleeping {sleep_seconds} seconds.") time.sleep(sleep_seconds) + def retrieve_next_available_scan_time(self): + """Retrieves the current time on the server and returns a datetime object of the next available scan time. If + the current time is 11:37:04, it would return 11:38. If the current time (11:37:46) is less than 15 seconds + before the next minute (11:38:00), a minute will be added as a buffer and 11:39 will be returned.""" + + # Retrieve the time on the server. + # "2021-05-13T10:29:16.644174-05:00" + server_time = self.retrieve_server_time() + + # Convert it from a string to a datetime object. + # datetime.datetime(2021, 5, 13, 10, 29, 16, 644174, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=68400))) + server_time_datetime = datetime.datetime.fromisoformat(server_time) + + # Extract the current minute. + # "29" + minute = server_time_datetime.minute + + # Build a future time object by adding 1 minute to the current server_time_datetime object. + server_time_future = server_time_datetime.replace(minute=(minute + 1)).replace(second=0).replace(microsecond=0) + + # If server_time_datetime is not within 15 seconds of server_time_future, use it as the next available scan + # datetime. + if (server_time_datetime + datetime.timedelta(seconds=15)) < server_time_future: + next_eligible_scan_datetime = server_time_future + + # server_time_datetime is within 15 seconds of server_time_future, add another minute as a buffer. + else: + next_eligible_scan_datetime = server_time_future.replace(minute=(minute + 2)) + + next_eligible_scan_string = f"{next_eligible_scan_datetime.hour}:{next_eligible_scan_datetime.minute}" + + return next_eligible_scan_string + if __name__ == "__main__": print("Use 'import scantron_api_client', do not run directly.") From bd99bf4a168ad41ea82ec085cc8f7df3a1280e20 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Thu, 13 May 2021 12:41:26 -0500 Subject: [PATCH 8/9] Removed extra whitespace --- scantron_api_client/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/scantron_api_client/README.md b/scantron_api_client/README.md index e0da008..3f4ad94 100644 --- a/scantron_api_client/README.md +++ b/scantron_api_client/README.md @@ -108,7 +108,6 @@ the scan through the web GUI, and inspect the POST request. Note there is a new ![recurrences](recurrences.png) ```python - # Option 1 - Scan at a future time. payload = { "site": 1, @@ -129,7 +128,6 @@ payload = { "recurrences": "RRULE:FREQ=WEEKLY;BYDAY=MO", } - response = sc.create_scan(payload) print(response.status_code) print(response.json()) From 440004f955b06be81e975781368758d0d5fd1856 Mon Sep 17 00:00:00 2001 From: derpadoo Date: Thu, 13 May 2021 14:17:32 -0500 Subject: [PATCH 9/9] Updated README --- README.md | 10 ++++++++++ console/scan_scheduler_visualizer.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 288787a..ef86560 100644 --- a/README.md +++ b/README.md @@ -587,6 +587,16 @@ the 1st or 9th ports. ![create_scan](./img/create_scan.png) + You can use the `console/scan_scheduler_visualizer.py` script found on the console to print out scheduled scan + times: + + ```bash + # Print out the scan start dates and times for all enabled scans in the next 10 days. + cd /home/scantron/console + source .venv/bin/activate + python scan_scheduler_visualizer.py -d 10 + ``` + 5. View currently executing scan results ```bash diff --git a/console/scan_scheduler_visualizer.py b/console/scan_scheduler_visualizer.py index 5acc7b3..af59fcd 100644 --- a/console/scan_scheduler_visualizer.py +++ b/console/scan_scheduler_visualizer.py @@ -32,7 +32,7 @@ def main(number_of_days_in_the_future=5, scan_id=None): # Standardize the exdates. Just a note: https://github.com/django-recurrence/django-recurrence/issues/70 for index, exdate in enumerate(scan.recurrences.exdates): updated_exdate = localtime(exdate).replace(hour=now_time.hour).replace(minute=now_time.minute) - print(f"Old exdate: {exdate} -- new exdate {updated_exdate}") + # print(f"Old exdate: {exdate} -- new exdate {updated_exdate}") scan.recurrences.exdates[index] = updated_exdate # datetime.datetime(2021, 5, 3, 0, 0)