@ -94,16 +94,6 @@ def init_app_secret(datastore_path):
return secret
return secret
# Remember python is by reference
# populate_form in wtfors didnt work for me. (try using a setattr() obj type on datastore.watch?)
def populate_form_from_watch ( form , watch ) :
for i in form . __dict__ . keys ( ) :
if i [ 0 ] != ' _ ' :
p = getattr ( form , i )
if hasattr ( p , ' data ' ) and i in watch :
setattr ( p , " data " , watch [ i ] )
# We use the whole watch object from the store/JSON so we can see if there's some related status in terms of a thread
# We use the whole watch object from the store/JSON so we can see if there's some related status in terms of a thread
# running or something similar.
# running or something similar.
@app.template_filter ( ' format_last_checked_time ' )
@app.template_filter ( ' format_last_checked_time ' )
@ -320,6 +310,7 @@ def changedetection_app(config=None, datastore_o=None):
guid = " {} / {} " . format ( watch [ ' uuid ' ] , watch [ ' last_changed ' ] )
guid = " {} / {} " . format ( watch [ ' uuid ' ] , watch [ ' last_changed ' ] )
fe = fg . add_entry ( )
fe = fg . add_entry ( )
# Include a link to the diff page, they will have to login here to see if password protection is enabled.
# Include a link to the diff page, they will have to login here to see if password protection is enabled.
# Description is the page you watch, link takes you to the diff JS UI page
# Description is the page you watch, link takes you to the diff JS UI page
base_url = datastore . data [ ' settings ' ] [ ' application ' ] [ ' base_url ' ]
base_url = datastore . data [ ' settings ' ] [ ' application ' ] [ ' base_url ' ]
@ -520,49 +511,46 @@ def changedetection_app(config=None, datastore_o=None):
@app.route ( " /edit/<string:uuid> " , methods = [ ' GET ' , ' POST ' ] )
@app.route ( " /edit/<string:uuid> " , methods = [ ' GET ' , ' POST ' ] )
@login_required
@login_required
# https://stackoverflow.com/questions/42984453/wtforms-populate-form-with-data-if-data-exists
# https://wtforms.readthedocs.io/en/3.0.x/forms/#wtforms.form.Form.populate_obj ?
def edit_page ( uuid ) :
def edit_page ( uuid ) :
from changedetectionio import forms
from changedetectionio import forms
form = forms . watchForm ( request . form )
# More for testing, possible to return the first/only
# More for testing, possible to return the first/only
if not datastore . data [ ' watching ' ] . keys ( ) :
flash ( " No watches to edit " , " error " )
return redirect ( url_for ( ' index ' ) )
if uuid == ' first ' :
if uuid == ' first ' :
uuid = list ( datastore . data [ ' watching ' ] . keys ( ) ) . pop ( )
uuid = list ( datastore . data [ ' watching ' ] . keys ( ) ) . pop ( )
if not uuid in datastore . data [ ' watching ' ] :
flash ( " No watch with the UUID %s found. " % ( uuid ) , " error " )
return redirect ( url_for ( ' index ' ) )
if request . method == ' GET ' :
form = forms . watchForm ( formdata = request . form if request . method == ' POST ' else None ,
if not uuid in datastore . data [ ' watching ' ] :
data = datastore . data [ ' watching ' ] [ uuid ]
flash ( " No watch with the UUID %s found. " % ( uuid ) , " error " )
)
return redirect ( url_for ( ' index ' ) )
populate_form_from_watch ( form , datastore . data [ ' watching ' ] [ uuid ] )
if request . method == ' GET ' :
# Set some defaults that refer to the main config when None, we do the same in POST,
# probably there should be a nice little handler for this.
if datastore . data [ ' watching ' ] [ uuid ] [ ' fetch_backend ' ] is None :
if datastore . data [ ' watching ' ] [ uuid ] [ ' fetch_backend ' ] is None :
form . fetch_backend . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' fetch_backend ' ]
form . fetch_backend . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' fetch_backend ' ]
if datastore . data [ ' watching ' ] [ uuid ] [ ' minutes_between_check ' ] is None :
form . minutes_between_check . data = datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ]
if request . method == ' POST ' and form . validate ( ) :
if request . method == ' POST ' and form . validate ( ) :
# Re #110, if they submit the same as the default value, set it to None, so we continue to follow the default
# Re #110, if they submit the same as the default value, set it to None, so we continue to follow the default
if form . minutes_between_check . data == datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ] :
if form . minutes_between_check . data == datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ] :
form . minutes_between_check . data = None
form . minutes_between_check . data = None
if form . fetch_backend . data == datastore . data [ ' settings ' ] [ ' application ' ] [ ' fetch_backend ' ] :
if form . fetch_backend . data == datastore . data [ ' settings ' ] [ ' application ' ] [ ' fetch_backend ' ] :
form . fetch_backend . data = None
form . fetch_backend . data = None
update_obj = { ' url ' : form . url . data . strip ( ) ,
extra_update_obj = { }
' minutes_between_check ' : form . minutes_between_check . data ,
' tag ' : form . tag . data . strip ( ) ,
' title ' : form . title . data . strip ( ) ,
' headers ' : form . headers . data ,
' body ' : form . body . data ,
' method ' : form . method . data ,
' ignore_status_codes ' : form . ignore_status_codes . data ,
' fetch_backend ' : form . fetch_backend . data ,
' trigger_text ' : form . trigger_text . data ,
' notification_title ' : form . notification_title . data ,
' notification_body ' : form . notification_body . data ,
' notification_format ' : form . notification_format . data ,
' extract_title_as_title ' : form . extract_title_as_title . data ,
}
# Notification URLs
# Notification URLs
datastore . data [ ' watching ' ] [ uuid ] [ ' notification_urls ' ] = form . notification_urls . data
datastore . data [ ' watching ' ] [ uuid ] [ ' notification_urls ' ] = form . notification_urls . data
@ -574,18 +562,15 @@ def changedetection_app(config=None, datastore_o=None):
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
if form_ignore_text :
if form_ignore_text :
if len ( datastore . data [ ' watching ' ] [ uuid ] [ ' history ' ] ) :
if len ( datastore . data [ ' watching ' ] [ uuid ] [ ' history ' ] ) :
update_obj [ ' previous_md5 ' ] = get_current_checksum_include_ignore_text ( uuid = uuid )
extra_update_obj [ ' previous_md5 ' ] = get_current_checksum_include_ignore_text ( uuid = uuid )
datastore . data [ ' watching ' ] [ uuid ] [ ' css_filter ' ] = form . css_filter . data . strip ( )
datastore . data [ ' watching ' ] [ uuid ] [ ' subtractive_selectors ' ] = form . subtractive_selectors . data
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
if form . css_filter . data . strip ( ) != datastore . data [ ' watching ' ] [ uuid ] [ ' css_filter ' ] :
if form . css_filter . data . strip ( ) != datastore . data [ ' watching ' ] [ uuid ] [ ' css_filter ' ] :
if len ( datastore . data [ ' watching ' ] [ uuid ] [ ' history ' ] ) :
if len ( datastore . data [ ' watching ' ] [ uuid ] [ ' history ' ] ) :
update_obj[ ' previous_md5 ' ] = get_current_checksum_include_ignore_text ( uuid = uuid )
extra_ update_obj[ ' previous_md5 ' ] = get_current_checksum_include_ignore_text ( uuid = uuid )
datastore . data [ ' watching ' ] [ uuid ] . update ( update_obj )
datastore . data [ ' watching ' ] [ uuid ] . update ( form . data )
datastore . data [ ' watching ' ] [ uuid ] . update ( extra_update_obj )
flash ( " Updated watch. " )
flash ( " Updated watch. " )
@ -610,17 +595,12 @@ def changedetection_app(config=None, datastore_o=None):
if request . method == ' POST ' and not form . validate ( ) :
if request . method == ' POST ' and not form . validate ( ) :
flash ( " An error occurred, please see below. " , " error " )
flash ( " An error occurred, please see below. " , " error " )
# Re #110 offer the default minutes
has_empty_checktime = datastore . data [ ' watching ' ] [ uuid ] . has_empty_checktime
using_default_minutes = False
if form . minutes_between_check . data == None :
form . minutes_between_check . data = datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ]
using_default_minutes = True
output = render_template ( " edit.html " ,
output = render_template ( " edit.html " ,
uuid = uuid ,
uuid = uuid ,
watch = datastore . data [ ' watching ' ] [ uuid ] ,
watch = datastore . data [ ' watching ' ] [ uuid ] ,
form = form ,
form = form ,
using_default_minutes= using_default_minutes ,
has_empty_checktime = has_empty_checktime ,
current_base_url = datastore . data [ ' settings ' ] [ ' application ' ] [ ' base_url ' ] ,
current_base_url = datastore . data [ ' settings ' ] [ ' application ' ] [ ' base_url ' ] ,
emailprefix = os . getenv ( ' NOTIFICATION_MAIL_BUTTON_PREFIX ' , False )
emailprefix = os . getenv ( ' NOTIFICATION_MAIL_BUTTON_PREFIX ' , False )
)
)
@ -630,61 +610,39 @@ def changedetection_app(config=None, datastore_o=None):
@app.route ( " /settings " , methods = [ ' GET ' , " POST " ] )
@app.route ( " /settings " , methods = [ ' GET ' , " POST " ] )
@login_required
@login_required
def settings_page ( ) :
def settings_page ( ) :
from changedetectionio import content_fetcher , forms
from changedetectionio import content_fetcher , forms
form = forms . globalSettingsForm ( request . form )
# Don't use form.data on POST so that it doesnt overrid the checkbox status from the POST status
form = forms . globalSettingsForm ( formdata = request . form if request . method == ' POST ' else None ,
data = datastore . data [ ' settings ' ]
)
if request . method == ' GET ' :
if request . method == ' POST ' :
form . minutes_between_check . data = int ( datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ] )
form . notification_urls . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_urls ' ]
form . global_subtractive_selectors . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' global_subtractive_selectors ' ]
form . global_ignore_text . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' global_ignore_text ' ]
form . ignore_whitespace . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' ignore_whitespace ' ]
form . render_anchor_tag_content . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' render_anchor_tag_content ' ]
form . extract_title_as_title . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' extract_title_as_title ' ]
form . fetch_backend . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' fetch_backend ' ]
form . notification_title . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_title ' ]
form . notification_body . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_body ' ]
form . notification_format . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_format ' ]
form . base_url . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' base_url ' ]
form . real_browser_save_screenshot . data = datastore . data [ ' settings ' ] [ ' application ' ] [ ' real_browser_save_screenshot ' ]
if request . method == ' POST ' and form . data . get ( ' removepassword_button ' ) == True :
# Password unset is a GET, but we can lock the session to a salted env password to always need the password
# Password unset is a GET, but we can lock the session to a salted env password to always need the password
if not os . getenv ( " SALTED_PASS " , False ) :
if form . application . form . data . get ( ' removepassword_button ' , False ) :
datastore . data [ ' settings ' ] [ ' application ' ] [ ' password ' ] = False
# SALTED_PASS means the password is "locked" to what we set in the Env var
flash ( " Password protection removed. " , ' notice ' )
if not os . getenv ( " SALTED_PASS " , False ) :
flask_login . logout_user ( )
datastore . remove_password ( )
return redirect ( url_for ( ' settings_page ' ) )
flash ( " Password protection removed. " , ' notice ' )
flask_login . logout_user ( )
return redirect ( url_for ( ' settings_page ' ) )
if form . validate ( ) :
datastore . data [ ' settings ' ] [ ' application ' ] . update ( form . data [ ' application ' ] )
datastore . data [ ' settings ' ] [ ' requests ' ] . update ( form . data [ ' requests ' ] )
datastore . needs_write = True
if request . method == ' POST ' and form . validate ( ) :
if not os . getenv ( " SALTED_PASS " , False ) and len ( form . application . form . password . encrypted_password ) :
datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_urls ' ] = form . notification_urls . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' password ' ] = form . application . form . password . encrypted_password
datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ] = form . minutes_between_check . data
datastore . needs_write = True
datastore . data [ ' settings ' ] [ ' application ' ] [ ' extract_title_as_title ' ] = form . extract_title_as_title . data
flash ( " Password protection enabled. " , ' notice ' )
datastore . data [ ' settings ' ] [ ' application ' ] [ ' fetch_backend ' ] = form . fetch_backend . data
flask_login . logout_user ( )
datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_title ' ] = form . notification_title . data
return redirect ( url_for ( ' index ' ) )
datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_body ' ] = form . notification_body . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_format ' ] = form . notification_format . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' notification_urls ' ] = form . notification_urls . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' base_url ' ] = form . base_url . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' global_subtractive_selectors ' ] = form . global_subtractive_selectors . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' global_ignore_text ' ] = form . global_ignore_text . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' ignore_whitespace ' ] = form . ignore_whitespace . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' real_browser_save_screenshot ' ] = form . real_browser_save_screenshot . data
datastore . data [ ' settings ' ] [ ' application ' ] [ ' render_anchor_tag_content ' ] = form . render_anchor_tag_content . data
if not os . getenv ( " SALTED_PASS " , False ) and form . password . encrypted_password :
datastore . data [ ' settings ' ] [ ' application ' ] [ ' password ' ] = form . password . encrypted_password
flash ( " Password protection enabled. " , ' notice ' )
flask_login . logout_user ( )
return redirect ( url_for ( ' index ' ) )
datastore . needs_write = True
flash ( " Settings updated. " )
flash ( " Settings updated. " )
if request . method == ' POST ' and not form . validate ( ) :
else :
flash ( " An error occurred, please see below. " , " error " )
flash ( " An error occurred, please see below. " , " error " )
output = render_template ( " settings.html " ,
output = render_template ( " settings.html " ,
form = form ,
form = form ,
@ -1172,8 +1130,6 @@ def notification_runner():
notification_debug_log = notification_debug_log [ - 100 : ]
notification_debug_log = notification_debug_log [ - 100 : ]
# Thread runner to check every minute, look for new watches to feed into the Queue.
# Thread runner to check every minute, look for new watches to feed into the Queue.
def ticker_thread_check_time_launch_checks ( ) :
def ticker_thread_check_time_launch_checks ( ) :
from changedetectionio import update_worker
from changedetectionio import update_worker
@ -1210,7 +1166,9 @@ def ticker_thread_check_time_launch_checks():
# Check for watches outside of the time threshold to put in the thread queue.
# Check for watches outside of the time threshold to put in the thread queue.
now = time . time ( )
now = time . time ( )
max_system_wide = int ( copied_datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ] ) * 60
recheck_time_minimum_seconds = int ( os . getenv ( ' MINIMUM_SECONDS_RECHECK_TIME ' , 60 ) )
recheck_time_system_seconds = int ( copied_datastore . data [ ' settings ' ] [ ' requests ' ] [ ' minutes_between_check ' ] ) * 60
for uuid , watch in copied_datastore . data [ ' watching ' ] . items ( ) :
for uuid , watch in copied_datastore . data [ ' watching ' ] . items ( ) :
@ -1219,18 +1177,14 @@ def ticker_thread_check_time_launch_checks():
continue
continue
# If they supplied an individual entry minutes to threshold.
# If they supplied an individual entry minutes to threshold.
watch_minutes_between_check = watch . get ( ' minutes_between_check ' , None )
threshold = now
if watch_minutes_between_check is not None :
if watch . threshold_seconds :
# Cast to int just incase
threshold - = watch . threshold_seconds
max_time = int ( watch_minutes_between_check ) * 60
else :
else :
# Default system wide.
threshold - = recheck_time_system_seconds
max_time = max_system_wide
threshold = now - max_time
# Yeah, put it in the queue, it's more than time
# Yeah, put it in the queue, it's more than time
if watch [ ' last_checked ' ] < = threshold :
if watch [ ' last_checked ' ] < = max ( threshold , recheck_time_minimum_seconds ) :
if not uuid in running_uuids and uuid not in update_q . queue :
if not uuid in running_uuids and uuid not in update_q . queue :
update_q . put ( uuid )
update_q . put ( uuid )
@ -1238,4 +1192,4 @@ def ticker_thread_check_time_launch_checks():
time . sleep ( 3 )
time . sleep ( 3 )
# Should be low so we can break this out in testing
# Should be low so we can break this out in testing
app . config . exit . wait ( 1 )
app . config . exit . wait ( 1 )