mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-10-31 14:47:21 +00:00 
			
		
		
		
	Compare commits
	
		
			12 Commits
		
	
	
		
			default-fo
			...
			hours-day-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b2b8c3f288 | ||
|   | 83add91f78 | ||
|   | fedb16c242 | ||
|   | 2d948ea6d1 | ||
|   | dee0c735e6 | ||
|   | 9fa98f4ec6 | ||
|   | b3b4b5d3f1 | ||
|   | a3f9ac0a6f | ||
|   | fcda5a0818 | ||
|   | 3920e613b9 | ||
|   | d023aa982e | ||
|   | c341baf71b | 
| @@ -566,23 +566,12 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|             for p in datastore.proxy_list: | ||||
|                 form.proxy.choices.append(tuple((p, datastore.proxy_list[p]['label']))) | ||||
|  | ||||
|  | ||||
|         if request.method == 'POST' and form.validate(): | ||||
|             extra_update_obj = {} | ||||
|  | ||||
|             if request.args.get('unpause_on_save'): | ||||
|                 extra_update_obj['paused'] = False | ||||
|  | ||||
|             # Re #110, if they submit the same as the default value, set it to None, so we continue to follow the default | ||||
|             # Assume we use the default value, unless something relevant is different, then use the form value | ||||
|             # values could be None, 0 etc. | ||||
|             # Set to None unless the next for: says that something is different | ||||
|             extra_update_obj['time_between_check'] = dict.fromkeys(form.time_between_check.data) | ||||
|             for k, v in form.time_between_check.data.items(): | ||||
|                 if v and v != datastore.data['settings']['requests']['time_between_check'][k]: | ||||
|                     extra_update_obj['time_between_check'] = form.time_between_check.data | ||||
|                     using_default_check_time = False | ||||
|                     break | ||||
|  | ||||
|             # Use the default if its the same as system wide | ||||
|             if form.fetch_backend.data == datastore.data['settings']['application']['fetch_backend']: | ||||
| @@ -732,13 +721,19 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|             else: | ||||
|                 flash("An error occurred, please see below.", "error") | ||||
|  | ||||
|         import datetime | ||||
|         datetime = datetime.datetime.now(pytz.timezone(datastore.data['settings']['application'].get('timezone'))) | ||||
|  | ||||
|         output = render_template("settings.html", | ||||
|                                  form=form, | ||||
|                                  current_base_url = datastore.data['settings']['application']['base_url'], | ||||
|                                  hide_remove_pass=os.getenv("SALTED_PASS", False), | ||||
|                                  api_key=datastore.data['settings']['application'].get('api_access_token'), | ||||
|                                  current_base_url=datastore.data['settings']['application']['base_url'], | ||||
|                                  datetime=str(datetime), | ||||
|                                  emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), | ||||
|                                  settings_application=datastore.data['settings']['application']) | ||||
|                                  form=form, | ||||
|                                  hide_remove_pass=os.getenv("SALTED_PASS", False), | ||||
|                                  settings_application=datastore.data['settings']['application'], | ||||
|                                  timezone=datastore.data['settings']['application'].get('timezone') | ||||
|                                  ) | ||||
|  | ||||
|         return output | ||||
|  | ||||
| @@ -1448,8 +1443,12 @@ def ticker_thread_check_time_launch_checks(): | ||||
|             seconds_since_last_recheck = now - watch['last_checked'] | ||||
|  | ||||
|             if seconds_since_last_recheck >= (threshold + watch.jitter_seconds) and seconds_since_last_recheck >= recheck_time_minimum_seconds: | ||||
|                 if not uuid in running_uuids and uuid not in [q_uuid for p,q_uuid in update_q.queue]: | ||||
|  | ||||
|                 if not watch.is_schedule_permitted: | ||||
|                     # Skip if the schedule (day of week and time) isnt permitted | ||||
|                     continue | ||||
|  | ||||
|                 if not uuid in running_uuids and uuid not in [q_uuid for p,q_uuid in update_q.queue]: | ||||
|                     # Proxies can be set to have a limit on seconds between which they can be called | ||||
|                     watch_proxy = datastore.get_preferred_proxy_for_watch(uuid=uuid) | ||||
|                     if watch_proxy and watch_proxy in list(datastore.proxy_list.keys()): | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import re | ||||
|  | ||||
| import pytz | ||||
| from wtforms import ( | ||||
|     BooleanField, | ||||
|     Field, | ||||
| @@ -8,9 +8,11 @@ from wtforms import ( | ||||
|     PasswordField, | ||||
|     RadioField, | ||||
|     SelectField, | ||||
|     SelectMultipleField, | ||||
|     StringField, | ||||
|     SubmitField, | ||||
|     TextAreaField, | ||||
|     TimeField, | ||||
|     fields, | ||||
|     validators, | ||||
|     widgets, | ||||
| @@ -97,6 +99,44 @@ class TimeBetweenCheckForm(Form): | ||||
|     seconds = IntegerField('Seconds', validators=[validators.Optional(), validators.NumberRange(min=0, message="Should contain zero or more seconds")]) | ||||
|     # @todo add total seconds minimum validatior = minimum_seconds_recheck_time | ||||
|  | ||||
| class MultiCheckboxDayOfWeekField(SelectMultipleField): | ||||
|     widget = widgets.ListWidget(prefix_label=False) | ||||
|     option_widget = widgets.CheckboxInput() | ||||
|  | ||||
| class TimeScheduleCheckLimitForm(Form): | ||||
|     # @todo must be a better python way todo this c/i list | ||||
|     c=[] | ||||
|     i=0 | ||||
|     for d in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']: | ||||
|         c.append((i, d)) | ||||
|         i+=1 | ||||
|     day_of_week = MultiCheckboxDayOfWeekField('',coerce=int, choices=c) | ||||
|     from_time = TimeField('From', validators=[validators.Optional()]) | ||||
|     until_time = TimeField('Until', validators=[validators.Optional()]) | ||||
|  | ||||
|     def validate(self, **kwargs): | ||||
|         if not super().validate(): | ||||
|             return False | ||||
|  | ||||
|         result = True | ||||
|  | ||||
|         f = self.data.get('from_time') | ||||
|         u = self.data.get('until_time') | ||||
|         if f and u: | ||||
|             import time | ||||
|             f = time.strptime(str(f), '%H:%M:%S') | ||||
|             u = time.strptime(str(u), '%H:%M:%S') | ||||
|             if f >= u: | ||||
|                 #@todo doesnt present | ||||
|                 self.from_time.errors.append('From time must be LESS than the until/end time') | ||||
|                 result = False | ||||
|  | ||||
|         if len(self.data.get('day_of_week', [])) == 0: | ||||
|             self.day_of_week.errors.append('No day selected') | ||||
|             result = False | ||||
|  | ||||
|         return result | ||||
|  | ||||
| # Separated by  key:value | ||||
| class StringDictKeyValue(StringField): | ||||
|     widget = widgets.TextArea() | ||||
| @@ -347,27 +387,22 @@ class watchForm(commonSettingsForm): | ||||
|     url = fields.URLField('URL', validators=[validateURL()]) | ||||
|     tag = StringField('Group tag', [validators.Optional()], default='') | ||||
|  | ||||
|     time_between_check = FormField(TimeBetweenCheckForm) | ||||
|  | ||||
|     include_filters = StringListField('CSS/JSONPath/JQ/XPath Filters', [ValidateCSSJSONXPATHInput()], default='') | ||||
|  | ||||
|     subtractive_selectors = StringListField('Remove elements', [ValidateCSSJSONXPATHInput(allow_xpath=False, allow_json=False)]) | ||||
|  | ||||
|     extract_text = StringListField('Extract text', [ValidateListRegex()]) | ||||
|  | ||||
|     title = StringField('Title', default='') | ||||
|  | ||||
|     ignore_text = StringListField('Ignore text', [ValidateListRegex()]) | ||||
|     headers = StringDictKeyValue('Request headers') | ||||
|     body = TextAreaField('Request body', [validators.Optional()]) | ||||
|     method = SelectField('Request method', choices=valid_method, default=default_method) | ||||
|     ignore_status_codes = BooleanField('Ignore status codes (process non-2xx status codes as normal)', default=False) | ||||
|     check_unique_lines = BooleanField('Only trigger when new lines appear', default=False) | ||||
|     trigger_text = StringListField('Trigger/wait for text', [validators.Optional(), ValidateListRegex()]) | ||||
|     extract_text = StringListField('Extract text', [ValidateListRegex()]) | ||||
|     headers = StringDictKeyValue('Request headers') | ||||
|     ignore_status_codes = BooleanField('Ignore status codes (process non-2xx status codes as normal)', default=False) | ||||
|     ignore_text = StringListField('Ignore text', [ValidateListRegex()]) | ||||
|     include_filters = StringListField('CSS/JSONPath/JQ/XPath Filters', [ValidateCSSJSONXPATHInput()], default='') | ||||
|     method = SelectField('Request method', choices=valid_method, default=default_method) | ||||
|     subtractive_selectors = StringListField('Remove elements', [ValidateCSSJSONXPATHInput(allow_xpath=False, allow_json=False)]) | ||||
|     text_should_not_be_present = StringListField('Block change-detection if text matches', [validators.Optional(), ValidateListRegex()]) | ||||
|  | ||||
|     time_between_check = FormField(TimeBetweenCheckForm) | ||||
|     time_schedule_check_limit = FormField(TimeScheduleCheckLimitForm) | ||||
|     time_use_system_default = BooleanField('Use system/default check time', default=False, validators=[validators.Optional()]) | ||||
|     title = StringField('Title', default='') | ||||
|     trigger_text = StringListField('Trigger/wait for text', [validators.Optional(), ValidateListRegex()]) | ||||
|     webdriver_js_execute_code = TextAreaField('Execute JavaScript before change detection', render_kw={"rows": "5"}, validators=[validators.Optional()]) | ||||
|  | ||||
|     save_button = SubmitField('Save', render_kw={"class": "pure-button pure-button-primary"}) | ||||
|  | ||||
|     proxy = RadioField('Proxy') | ||||
| @@ -389,10 +424,10 @@ class watchForm(commonSettingsForm): | ||||
|  | ||||
|         return result | ||||
|  | ||||
|  | ||||
| # datastore.data['settings']['requests'].. | ||||
| class globalSettingsRequestForm(Form): | ||||
|     time_between_check = FormField(TimeBetweenCheckForm) | ||||
|     time_schedule_check_limit = FormField(TimeScheduleCheckLimitForm) | ||||
|     proxy = RadioField('Proxy') | ||||
|     jitter_seconds = IntegerField('Random jitter seconds ± check', | ||||
|                                   render_kw={"style": "width: 5em;"}, | ||||
| @@ -401,21 +436,21 @@ class globalSettingsRequestForm(Form): | ||||
| # datastore.data['settings']['application'].. | ||||
| class globalSettingsApplicationForm(commonSettingsForm): | ||||
|  | ||||
|     base_url = StringField('Base URL', validators=[validators.Optional()]) | ||||
|     global_subtractive_selectors = StringListField('Remove elements', [ValidateCSSJSONXPATHInput(allow_xpath=False, allow_json=False)]) | ||||
|     global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()]) | ||||
|     ignore_whitespace = BooleanField('Ignore whitespace') | ||||
|     removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"}) | ||||
|     empty_pages_are_a_change =  BooleanField('Treat empty pages as a change?', default=False) | ||||
|     render_anchor_tag_content = BooleanField('Render anchor tag content', default=False) | ||||
|     fetch_backend = RadioField('Fetch Method', default="html_requests", choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) | ||||
|     api_access_token_enabled = BooleanField('API access token security check enabled', default=True, validators=[validators.Optional()]) | ||||
|     password = SaltyPasswordField() | ||||
|  | ||||
|     base_url = StringField('Base URL', validators=[validators.Optional()]) | ||||
|     empty_pages_are_a_change =  BooleanField('Treat empty pages as a change?', default=False) | ||||
|     fetch_backend = RadioField('Fetch Method', default="html_requests", choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) | ||||
|     filter_failure_notification_threshold_attempts = IntegerField('Number of times the filter can be missing before sending a notification', | ||||
|                                                                   render_kw={"style": "width: 5em;"}, | ||||
|                                                                   validators=[validators.NumberRange(min=0, | ||||
|                                                                                                      message="Should contain zero or more attempts")]) | ||||
|     global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()]) | ||||
|     global_subtractive_selectors = StringListField('Remove elements', [ValidateCSSJSONXPATHInput(allow_xpath=False, allow_json=False)]) | ||||
|     ignore_whitespace = BooleanField('Ignore whitespace') | ||||
|     password = SaltyPasswordField() | ||||
|     removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"}) | ||||
|     render_anchor_tag_content = BooleanField('Render anchor tag content', default=False) | ||||
|     timezone = SelectField('Timezone', choices=pytz.all_timezones) | ||||
|  | ||||
|  | ||||
| class globalSettingsForm(Form): | ||||
|   | ||||
| @@ -17,29 +17,31 @@ class model(dict): | ||||
|                 'requests': { | ||||
|                     'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")),  # Default 45 seconds | ||||
|                     'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None}, | ||||
|                     'time_schedule_check_limit': {'day_of_week': [0, 1, 2, 3, 4, 5, 6], 'time_from': '', 'time_until': ''}, | ||||
|                     'jitter_seconds': 0, | ||||
|                     'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "10")),  # Number of threads, lower is better for slow connections | ||||
|                     'proxy': None # Preferred proxy connection | ||||
|                 }, | ||||
|                 'application': { | ||||
|                     # Custom notification content | ||||
|                     'api_access_token_enabled': True, | ||||
|                     'password': False, | ||||
|                     'base_url' : None, | ||||
|                     'extract_title_as_title': False, | ||||
|                     'empty_pages_are_a_change': False, | ||||
|                     'extract_title_as_title': False, | ||||
|                     'fetch_backend': getenv("DEFAULT_FETCH_BACKEND", "html_requests"), | ||||
|                     'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT, | ||||
|                     'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum | ||||
|                     'global_subtractive_selectors': [], | ||||
|                     'ignore_whitespace': True, | ||||
|                     'render_anchor_tag_content': False, | ||||
|                     'notification_urls': [], # Apprise URL list | ||||
|                     # Custom notification content | ||||
|                     'notification_title': default_notification_title, | ||||
|                     'notification_body': default_notification_body, | ||||
|                     'notification_format': default_notification_format, | ||||
|                     'notification_title': default_notification_title, | ||||
|                     'notification_urls': [], # Apprise URL list | ||||
|                     'password': False, | ||||
|                     'render_anchor_tag_content': False, | ||||
|                     'schema_version' : 0, | ||||
|                     'webdriver_delay': None  # Extra delay in seconds before extracting text | ||||
|                     'timezone': 'UTC', | ||||
|                     'webdriver_delay': None,  # Extra delay in seconds before extracting text | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -49,6 +49,7 @@ class model(dict): | ||||
|             # Requires setting to None on submit if it's the same as the default | ||||
|             # Should be all None by default, so we use the system default in this case. | ||||
|             'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None}, | ||||
|             'time_schedule_check_limit': {'day_of_week': [0, 1, 2, 3, 4, 5, 6], 'time_from': '', 'time_until': ''}, | ||||
|             'title': None, | ||||
|             'trigger_text': [],  # List of text or regex to wait for until a change is detected | ||||
|             'url': None, | ||||
| @@ -228,6 +229,11 @@ class model(dict): | ||||
|                 seconds += x * n | ||||
|         return seconds | ||||
|  | ||||
|     def is_schedule_permitted(self): | ||||
|         """According to the current day of week and time, is this watch queueable?""" | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     # Iterate over all history texts and see if something new exists | ||||
|     def lines_contain_something_unique_compared_to_history(self, lines: list): | ||||
|         local_lines = set([l.decode('utf-8').strip().lower() for l in lines]) | ||||
|   | ||||
| @@ -132,7 +132,7 @@ body:after, body:before { | ||||
|  | ||||
| .fetch-error { | ||||
|   padding-top: 1em; | ||||
|   font-size: 60%; | ||||
|   font-size: 80%; | ||||
|   max-width: 400px; | ||||
|   display: block; } | ||||
|  | ||||
| @@ -480,6 +480,22 @@ ul { | ||||
|   .time-check-widget tr input[type="number"] { | ||||
|     width: 5em; } | ||||
|  | ||||
| .pure-control-group table label { | ||||
|   color: #333; | ||||
|   font-weight: normal; } | ||||
|  | ||||
| .time-schedule-check-limit-widget tr { | ||||
|   display: inline-block; } | ||||
|  | ||||
| .time-schedule-check-limit-widget li { | ||||
|   text-decoration: none; } | ||||
|  | ||||
| .time-schedule-check-limit-widget ul { | ||||
|   padding-left: 0px; } | ||||
|   .time-schedule-check-limit-widget ul li { | ||||
|     display: inline-block; | ||||
|     width: 3em; } | ||||
|  | ||||
| #selector-wrapper { | ||||
|   height: 600px; | ||||
|   overflow-y: scroll; | ||||
|   | ||||
| @@ -677,6 +677,29 @@ ul { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| .pure-control-group table  label { | ||||
|   color: #333; | ||||
|   font-weight: normal; | ||||
| } | ||||
|  | ||||
| .time-schedule-check-limit-widget { | ||||
|   tr { | ||||
|     display: inline-block; | ||||
|   } | ||||
|  | ||||
|   li { | ||||
|     text-decoration: none; | ||||
|   } | ||||
|  | ||||
|   ul { | ||||
|     padding-left: 0px; | ||||
|     li { | ||||
|       display: inline-block; | ||||
|       width: 3em; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| #selector-wrapper { | ||||
|  height: 600px; | ||||
|   | ||||
| @@ -51,14 +51,15 @@ | ||||
|                         <span class="pure-form-message-inline">Organisational tag/group name used in the main listing page</span> | ||||
|                     </div> | ||||
|                     <div class="pure-control-group"> | ||||
|                         {{ render_checkbox_field(form.time_use_system_default) }} | ||||
|                         <div style="opacity: 0.5"> | ||||
|                         {{ render_field(form.time_between_check, class="time-check-widget") }} | ||||
|                         {{ render_field(form.time_schedule_check_limit, class="time-schedule-check-limit-widget") }} | ||||
| @todo - add 'use default' checkbox | ||||
|                         </div> | ||||
|                         {% if has_empty_checktime %} | ||||
|                         <span class="pure-form-message-inline">Currently using the <a | ||||
|                                 href="{{ url_for('settings_page', uuid=uuid) }}">default global settings</a>, change to another value if you want to be specific.</span> | ||||
|                         {% else %} | ||||
|                         <span class="pure-form-message-inline">Set to blank to use the <a | ||||
|                                 href="{{ url_for('settings_page', uuid=uuid) }}">default global settings</a>.</span> | ||||
|                         {% endif %} | ||||
|  | ||||
|                     </div> | ||||
|                     <div class="pure-control-group"> | ||||
|                         {{ render_checkbox_field(form.extract_title_as_title) }} | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
|             <li class="tab"><a href="#fetching">Fetching</a></li> | ||||
|             <li class="tab"><a href="#filters">Global Filters</a></li> | ||||
|             <li class="tab"><a href="#api">API</a></li> | ||||
|             <li class="tab"><a href="#date-time">Date & Time</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|     <div class="box-wrap inner"> | ||||
| @@ -30,6 +31,7 @@ | ||||
|                 <fieldset> | ||||
|                     <div class="pure-control-group"> | ||||
|                         {{ render_field(form.requests.form.time_between_check, class="time-check-widget") }} | ||||
|                         {{ render_field(form.requests.form.time_schedule_check_limit, class="time-schedule-check-limit-widget") }} | ||||
|                         <span class="pure-form-message-inline">Default time for all watches, when the watch does not have a specific time setting.</span> | ||||
|                     </div> | ||||
|                     <div class="pure-control-group"> | ||||
| @@ -91,7 +93,6 @@ | ||||
|                     </div> | ||||
|                 </fieldset> | ||||
|             </div> | ||||
|  | ||||
|             <div class="tab-pane-inner" id="fetching"> | ||||
|                 <div class="pure-control-group inline-radio"> | ||||
|                     {{ render_field(form.application.form.fetch_backend, class="fetch-backend") }} | ||||
| @@ -170,6 +171,19 @@ nav | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="tab-pane-inner" id="date-time"> | ||||
|                 <fieldset> | ||||
|                     <div class="field-group"> | ||||
|                         {{ render_field(form.application.form.timezone) }} | ||||
|                     </div> | ||||
|                     <div class="field-group"> | ||||
|                         <p> | ||||
|                         <label>Local time</label> {{ datetime }}<br/> | ||||
|                         <label>Configured timezone:</label> {{ timezone }}<br/> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                 </fieldset> | ||||
|             </div> | ||||
|  | ||||
|             <div id="actions"> | ||||
|                 <div class="pure-control-group"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user