Compare commits

...

44 Commits

Author SHA1 Message Date
dgtlmoon
ed584b38bf API Access should be limited by preference 2025-03-23 00:23:28 +01:00
dgtlmoon
46d11f3d70 Re #3045 - API Access should still work even when UI Password is enabled 2025-03-23 00:11:04 +01:00
dgtlmoon
10b2bbea83 0.49.5 2025-03-22 22:51:33 +01:00
dgtlmoon
32d110b92f Template tidyup & UI Fixes (#3044) 2025-03-22 22:48:01 +01:00
dgtlmoon
860a5f5c1a Watch history - Ensure atomic/safe history data disk writes (#3042 #3041)
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-03-22 19:16:08 +01:00
Nico Ell
70a18ee4b5 Testing - Replace Linux only 'resource' library with cross-platform 'psutil' library (#3037)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-03-21 09:50:32 +01:00
dgtlmoon
73189672c3 Refactor code layout, add extra tests
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-03-18 10:40:22 +01:00
dgtlmoon
7e7d5dc383 New major functionality CONDITIONS - Compare values, check numbers within range, etc
Some checks failed
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
2025-03-17 19:20:24 +01:00
dgtlmoon
1c2cfc37aa 0.49.4
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-03-13 12:06:50 +01:00
dgtlmoon
0634fe021d Datastore - Always use utf-8 encoding for error text output storage
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-03-08 19:20:45 +01:00
boustea
04934b6b3b Restock detection - Adding french keywords for out of stock items
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-03-07 10:14:39 +01:00
dgtlmoon
ff00417bc5 Browser Steps - Should use the Watch URL/link after any Jinja2 type templates are applied
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-02-27 17:22:39 +01:00
dgtlmoon
849c5b2293 BrowserSteps - Speed up scraping, refactor screenshot handling for very long pages (#2999)
Some checks failed
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
2025-02-27 16:52:38 +01:00
dgtlmoon
4bf560256b Browser Steps - Added new "Make all child elements visible" action
Some checks failed
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-02-26 23:12:02 +01:00
dgtlmoon
7903b03a0c Browser Steps - Added new "Remove elements" action 2025-02-26 22:37:06 +01:00
dgtlmoon
5e7c0880c1 UI - Browser Steps - "Click X,Y" should focus on the input field also 2025-02-26 22:29:31 +01:00
dgtlmoon
957aef4ff3 UI - Browser Steps - Improving Browser Steps usability on mobile 2025-02-26 22:23:47 +01:00
dgtlmoon
8e9a83d8f4 0.49.3
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-02-22 10:24:44 +01:00
dgtlmoon
5961838143 UI - Reverting JS change to tabs (the better fix was the W3C HTML validation) 2025-02-22 10:22:25 +01:00
dgtlmoon
8cf4a8128b 0.49.2
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-02-19 16:01:01 +01:00
dgtlmoon
24c3bfe5ad UI - Make the setup and error messages for Visual Selector and Browser Steps a lot more meaningful (#2977) 2025-02-19 14:18:18 +01:00
dgtlmoon
bdd9760f3c Update docker-compose.yml
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-19 10:46:22 +01:00
dgtlmoon
e37467f649 UI - More W3C HTML validation fixes 2025-02-19 10:44:54 +01:00
dgtlmoon
d42fdf0257 UI - More W3C validation fixes (#2973)
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-18 11:02:05 +01:00
dgtlmoon
939fa86582 UI - Tweaks for HTML validation 2025-02-18 10:17:19 +01:00
dgtlmoon
b87c92b9e0 Filter - "Unique lines" could possibly crash if history was empty or cleared on the disk
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-17 22:27:55 +01:00
dgtlmoon
4d5535d72c UI - Sometimes the DOM wasnt ready when tab selection triggered via CSS, which displayed empty tabs on some browsers 2025-02-17 22:15:01 +01:00
panzli
ad08219d03 Removing deprecated docker-compose.yml version attribute (#2967) 2025-02-17 22:05:09 +01:00
dgtlmoon
82211eef82 Update settings.html
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-02-11 11:15:13 +01:00
dgtlmoon
5d9380609c Browser Steps - Increasing timeout for actions and unifying timeout values
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-10 10:56:44 +01:00
dgtlmoon
a8b3918fca Browser Steps - Fixing 'Uncheck checkbox' #2958 2025-02-10 10:49:40 +01:00
dgtlmoon
e83fb37fb6 UI - "Browser Steps" tab should be always available with helpful info (evenwhen playwright is not configured) (#2955)
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-10 00:36:35 +01:00
dgtlmoon
6b99afe0f7 Adding browser_steps JSON Schema rule for API updates (#2957) 2025-02-10 00:35:39 +01:00
dgtlmoon
09ebc6ec63 UI - Fix mute/unmute alt/title label alt/title text in watch overview (#2951)
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-08 18:01:26 +01:00
dgtlmoon
6b1065502e 0.49.1
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-08 10:14:19 +01:00
vin86
d4c470984a Update stock-not-in-stock.js - Italian (#2948)
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-08 00:23:20 +01:00
dgtlmoon
55da48f719 Re #2945 - Handle/Strip UTF-8 ByteOrderMark in JSON strings correctly (fixes "Exception: No parsable JSON found in this document" error) (#2947)
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-02-07 22:19:23 +01:00
RoboMagus
dbd4adf23a Add major and minor tags for Docker release workflow (#2938)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-02-01 10:52:04 +01:00
dgtlmoon
b1e700b3ff Adding jinja2/browsersteps test (#2915)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-01-28 18:14:49 +01:00
Iftekhar Alam Fuad
1c61b5a623 Header handling - Fix header parsing to split on the first colon only (headers where the value contained :// type may have been broken) (#2929)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-01-26 00:08:09 +01:00
dgtlmoon
e799a1cdcb 0.49.00
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-01-21 13:40:01 +01:00
dgtlmoon
938065db6f Update README.md
Some checks are pending
Build and push containers / metadata (push) Waiting to run
Build and push containers / build-push-containers (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Waiting to run
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Blocked by required conditions
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Blocked by required conditions
ChangeDetection.io App Test / lint-code (push) Waiting to run
ChangeDetection.io App Test / test-application-3-10 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-11 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-12 (push) Blocked by required conditions
ChangeDetection.io App Test / test-application-3-13 (push) Blocked by required conditions
2025-01-20 16:10:54 +01:00
dgtlmoon
4f2d38ff49 Build/Libraries - Pin referencing library which breaks due to out-dated flask_expects_json, remove pip upgrade in test(#2912)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-01-18 23:20:58 +01:00
dgtlmoon
8960f401b7 Notifications - Custom POST:// GET:// etc endpoints - returning 204 and other 20x responses are OK (don't show an error was detected)(#2897)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-01-13 13:13:18 +01:00
128 changed files with 3586 additions and 2116 deletions

View File

@@ -103,6 +103,19 @@ jobs:
# provenance: false # provenance: false
# A new tagged release is required, which builds :tag and :latest # A new tagged release is required, which builds :tag and :latest
- name: Docker meta :tag
if: github.event_name == 'release' && startsWith(github.event.release.tag_name, '0.')
uses: docker/metadata-action@v5
id: meta
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io
ghcr.io/dgtlmoon/changedetection.io
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push :tag - name: Build and push :tag
id: docker_build_tag_release id: docker_build_tag_release
if: github.event_name == 'release' && startsWith(github.event.release.tag_name, '0.') if: github.event_name == 'release' && startsWith(github.event.release.tag_name, '0.')
@@ -111,11 +124,7 @@ jobs:
context: ./ context: ./
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: | tags: ${{ steps.meta.outputs.tags }}
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:${{ github.event.release.tag_name }}
ghcr.io/dgtlmoon/changedetection.io:${{ github.event.release.tag_name }}
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:latest
ghcr.io/dgtlmoon/changedetection.io:latest
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,linux/arm64/v8 platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View File

@@ -45,9 +45,12 @@ jobs:
- name: Test that the basic pip built package runs without error - name: Test that the basic pip built package runs without error
run: | run: |
set -ex set -ex
sudo pip3 install --upgrade pip ls -alR
pip3 install dist/changedetection.io*.whl
# Find and install the first .whl file
find dist -type f -name "*.whl" -exec pip3 install {} \; -quit
changedetection.io -d /tmp -p 10000 & changedetection.io -d /tmp -p 10000 &
sleep 3 sleep 3
curl --retry-connrefused --retry 6 http://127.0.0.1:10000/static/styles/pure-min.css >/dev/null curl --retry-connrefused --retry 6 http://127.0.0.1:10000/static/styles/pure-min.css >/dev/null
curl --retry-connrefused --retry 6 http://127.0.0.1:10000/ >/dev/null curl --retry-connrefused --retry 6 http://127.0.0.1:10000/ >/dev/null

View File

@@ -64,14 +64,16 @@ jobs:
echo "Running processes in docker..." echo "Running processes in docker..."
docker ps docker ps
- name: Test built container with Pytest (generally as requests/plaintext fetching) - name: Run Unit Tests
run: | run: |
# Unit tests # Unit tests
echo "run test with unittest"
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff' docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model' docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_watch_model'
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security' docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_semver'
- name: Test built container with Pytest (generally as requests/plaintext fetching)
run: |
# All tests # All tests
echo "run test with pytest" echo "run test with pytest"
# The default pytest logger_level is TRACE # The default pytest logger_level is TRACE

View File

@@ -2,6 +2,7 @@ recursive-include changedetectionio/api *
recursive-include changedetectionio/apprise_plugin * recursive-include changedetectionio/apprise_plugin *
recursive-include changedetectionio/blueprint * recursive-include changedetectionio/blueprint *
recursive-include changedetectionio/content_fetchers * recursive-include changedetectionio/content_fetchers *
recursive-include changedetectionio/conditions *
recursive-include changedetectionio/model * recursive-include changedetectionio/model *
recursive-include changedetectionio/processors * recursive-include changedetectionio/processors *
recursive-include changedetectionio/static * recursive-include changedetectionio/static *

View File

@@ -120,7 +120,7 @@ Easily add the current web page to your changedetection.io tool, simply install
[<img src="./docs/chrome-extension-screenshot.png" style="max-width:80%;" alt="Chrome Extension to easily add the current web-page to detect a change." title="Chrome Extension to easily add the current web-page to detect a change." />](https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop) [<img src="./docs/chrome-extension-screenshot.png" style="max-width:80%;" alt="Chrome Extension to easily add the current web-page to detect a change." title="Chrome Extension to easily add the current web-page to detect a change." />](https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop)
[Goto the Chrome Webstore to download the extension.](https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop) [Goto the Chrome Webstore to download the extension.](https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop) ( Or check out the [GitHub repo](https://github.com/dgtlmoon/changedetection.io-browser-extension) )
## Installation ## Installation

View File

@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki # Read more https://github.com/dgtlmoon/changedetection.io/wiki
__version__ = '0.48.06' __version__ = '0.49.5'
from changedetectionio.strtobool import strtobool from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
@@ -24,6 +24,9 @@ from loguru import logger
app = None app = None
datastore = None datastore = None
def get_version():
return __version__
# Parent wrapper or OS sends us a SIGTERM/SIGINT, do everything required for a clean shutdown # Parent wrapper or OS sends us a SIGTERM/SIGINT, do everything required for a clean shutdown
def sigshutdown_handler(_signo, _stack_frame): def sigshutdown_handler(_signo, _stack_frame):
global app global app

View File

@@ -112,6 +112,35 @@ def build_watch_json_schema(d):
schema['properties']['time_between_check'] = build_time_between_check_json_schema() schema['properties']['time_between_check'] = build_time_between_check_json_schema()
schema['properties']['browser_steps'] = {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"operation": {
"type": ["string", "null"],
"maxLength": 5000 # Allows null and any string up to 5000 chars (including "")
},
"selector": {
"type": ["string", "null"],
"maxLength": 5000
},
"optional_value": {
"type": ["string", "null"],
"maxLength": 5000
}
},
"required": ["operation", "selector", "optional_value"],
"additionalProperties": False # No extra keys allowed
}
},
{"type": "null"}, # Allows null for `browser_steps`
{"type": "array", "maxItems": 0} # Allows empty array []
]
}
# headers ? # headers ?
return schema return schema

View File

@@ -285,8 +285,6 @@ class CreateWatch(Resource):
list = {} list = {}
tag_limit = request.args.get('tag', '').lower() tag_limit = request.args.get('tag', '').lower()
for uuid, watch in self.datastore.data['watching'].items(): for uuid, watch in self.datastore.data['watching'].items():
# Watch tags by name (replace the other calls?) # Watch tags by name (replace the other calls?)
tags = self.datastore.get_all_tags_for_watch(uuid=uuid) tags = self.datastore.get_all_tags_for_watch(uuid=uuid)

View File

@@ -11,22 +11,14 @@ def check_token(f):
datastore = args[0].datastore datastore = args[0].datastore
config_api_token_enabled = datastore.data['settings']['application'].get('api_access_token_enabled') config_api_token_enabled = datastore.data['settings']['application'].get('api_access_token_enabled')
if not config_api_token_enabled:
return
try:
api_key_header = request.headers['x-api-key']
except KeyError:
return make_response(
jsonify("No authorization x-api-key header."), 403
)
config_api_token = datastore.data['settings']['application'].get('api_access_token') config_api_token = datastore.data['settings']['application'].get('api_access_token')
if api_key_header != config_api_token: # config_api_token_enabled - a UI option in settings if access should obey the key or not
return make_response( if config_api_token_enabled:
jsonify("Invalid access - API key invalid."), 403 if request.headers.get('x-api-key') != config_api_token:
) return make_response(
jsonify("Invalid access - API key invalid."), 403
)
return f(*args, **kwargs) return f(*args, **kwargs)

View File

@@ -27,19 +27,17 @@ def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
method = re.sub(rf's$', '', schema) method = re.sub(rf's$', '', schema)
requests_method = getattr(requests, method) requests_method = getattr(requests, method)
headers = CaseInsensitiveDict({})
params = CaseInsensitiveDict({}) # Added to requests params = CaseInsensitiveDict({}) # Added to requests
auth = None auth = None
has_error = False has_error = False
# Convert /foobar?+some-header=hello to proper header dictionary # Convert /foobar?+some-header=hello to proper header dictionary
results = apprise_parse_url(url) results = apprise_parse_url(url)
# Add our headers that the user can potentially over-ride if they wish # Add our headers that the user can potentially over-ride if they wish
# to to our returned result set and tidy entries by unquoting them # to to our returned result set and tidy entries by unquoting them
headers = {unquote_plus(x): unquote_plus(y) headers = CaseInsensitiveDict({unquote_plus(x): unquote_plus(y)
for x, y in results['qsd+'].items()} for x, y in results['qsd+'].items()})
# https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation
# In Apprise, it relies on prefixing each request arg with "-", because it uses say &method=update as a flag for apprise # In Apprise, it relies on prefixing each request arg with "-", because it uses say &method=update as a flag for apprise
@@ -81,7 +79,7 @@ def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
params=params params=params
) )
if r.status_code not in (requests.codes.created, requests.codes.ok): if not (200 <= r.status_code < 300):
status_str = f"Error sending '{method.upper()}' request to {url} - Status: {r.status_code}: '{r.reason}'" status_str = f"Error sending '{method.upper()}' request to {url} - Status: {r.status_code}: '{r.reason}'"
logger.error(status_str) logger.error(status_str)
has_error = True has_error = True

View File

@@ -0,0 +1,36 @@
import os
from functools import wraps
from flask import current_app, redirect, request
from loguru import logger
def login_optionally_required(func):
"""
If password authentication is enabled, verify the user is logged in.
To be used as a decorator for routes that should optionally require login.
This version is blueprint-friendly as it uses current_app instead of directly accessing app.
"""
@wraps(func)
def decorated_view(*args, **kwargs):
from flask import current_app
import flask_login
from flask_login import current_user
# Access datastore through the app config
datastore = current_app.config['DATASTORE']
has_password_enabled = datastore.data['settings']['application'].get('password') or os.getenv("SALTED_PASS", False)
# Permitted
if request.endpoint and 'static_content' in request.endpoint and request.view_args and request.view_args.get('group') == 'styles':
return func(*args, **kwargs)
# Permitted
elif request.endpoint and 'diff_history_page' in request.endpoint and datastore.data['settings']['application'].get('shared_diff_access'):
return func(*args, **kwargs)
elif request.method in flask_login.config.EXEMPT_METHODS:
return func(*args, **kwargs)
elif current_app.config.get('LOGIN_DISABLED'):
return func(*args, **kwargs)
elif has_password_enabled and not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
return func(*args, **kwargs)
return decorated_view

View File

@@ -22,7 +22,10 @@ from loguru import logger
browsersteps_sessions = {} browsersteps_sessions = {}
io_interface_context = None io_interface_context = None
import json
import base64
import hashlib
from flask import Response
def construct_blueprint(datastore: ChangeDetectionStore): def construct_blueprint(datastore: ChangeDetectionStore):
browser_steps_blueprint = Blueprint('browser_steps', __name__, template_folder="templates") browser_steps_blueprint = Blueprint('browser_steps', __name__, template_folder="templates")
@@ -85,7 +88,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
browsersteps_start_session['browserstepper'] = browser_steps.browsersteps_live_ui( browsersteps_start_session['browserstepper'] = browser_steps.browsersteps_live_ui(
playwright_browser=browsersteps_start_session['browser'], playwright_browser=browsersteps_start_session['browser'],
proxy=proxy, proxy=proxy,
start_url=datastore.data['watching'][watch_uuid].get('url'), start_url=datastore.data['watching'][watch_uuid].link,
headers=datastore.data['watching'][watch_uuid].get('headers') headers=datastore.data['watching'][watch_uuid].get('headers')
) )
@@ -160,14 +163,13 @@ def construct_blueprint(datastore: ChangeDetectionStore):
if not browsersteps_sessions.get(browsersteps_session_id): if not browsersteps_sessions.get(browsersteps_session_id):
return make_response('No session exists under that ID', 500) return make_response('No session exists under that ID', 500)
is_last_step = False
# Actions - step/apply/etc, do the thing and return state # Actions - step/apply/etc, do the thing and return state
if request.method == 'POST': if request.method == 'POST':
# @todo - should always be an existing session # @todo - should always be an existing session
step_operation = request.form.get('operation') step_operation = request.form.get('operation')
step_selector = request.form.get('selector') step_selector = request.form.get('selector')
step_optional_value = request.form.get('optional_value') step_optional_value = request.form.get('optional_value')
step_n = int(request.form.get('step_n'))
is_last_step = strtobool(request.form.get('is_last_step')) is_last_step = strtobool(request.form.get('is_last_step'))
# @todo try.. accept.. nice errors not popups.. # @todo try.. accept.. nice errors not popups..
@@ -182,16 +184,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
# Try to find something of value to give back to the user # Try to find something of value to give back to the user
return make_response(str(e).splitlines()[0], 401) return make_response(str(e).splitlines()[0], 401)
# Get visual selector ready/update its data (also use the current filter info from the page?)
# When the last 'apply' button was pressed
# @todo this adds overhead because the xpath selection is happening twice
u = browsersteps_sessions[browsersteps_session_id]['browserstepper'].page.url
if is_last_step and u:
(screenshot, xpath_data) = browsersteps_sessions[browsersteps_session_id]['browserstepper'].request_visualselector_data()
watch = datastore.data['watching'].get(uuid)
if watch:
watch.save_screenshot(screenshot=screenshot)
watch.save_xpath_data(data=xpath_data)
# if not this_session.page: # if not this_session.page:
# cleanup_playwright_session() # cleanup_playwright_session()
@@ -199,31 +191,35 @@ def construct_blueprint(datastore: ChangeDetectionStore):
# Screenshots and other info only needed on requesting a step (POST) # Screenshots and other info only needed on requesting a step (POST)
try: try:
state = browsersteps_sessions[browsersteps_session_id]['browserstepper'].get_current_state() (screenshot, xpath_data) = browsersteps_sessions[browsersteps_session_id]['browserstepper'].get_current_state()
if is_last_step:
watch = datastore.data['watching'].get(uuid)
u = browsersteps_sessions[browsersteps_session_id]['browserstepper'].page.url
if watch and u:
watch.save_screenshot(screenshot=screenshot)
watch.save_xpath_data(data=xpath_data)
except playwright._impl._api_types.Error as e: except playwright._impl._api_types.Error as e:
return make_response("Browser session ran out of time :( Please reload this page."+str(e), 401) return make_response("Browser session ran out of time :( Please reload this page."+str(e), 401)
except Exception as e:
return make_response("Error fetching screenshot and element data - " + str(e), 401)
# Use send_file() which is way faster than read/write loop on bytes # SEND THIS BACK TO THE BROWSER
import json
from tempfile import mkstemp
from flask import send_file
tmp_fd, tmp_file = mkstemp(text=True, suffix=".json", prefix="changedetectionio-")
output = json.dumps({'screenshot': "data:image/jpeg;base64,{}".format( output = {
base64.b64encode(state[0]).decode('ascii')), "screenshot": f"data:image/jpeg;base64,{base64.b64encode(screenshot).decode('ascii')}",
'xpath_data': state[1], "xpath_data": xpath_data,
'session_age_start': browsersteps_sessions[browsersteps_session_id]['browserstepper'].age_start, "session_age_start": browsersteps_sessions[browsersteps_session_id]['browserstepper'].age_start,
'browser_time_remaining': round(remaining) "browser_time_remaining": round(remaining)
}) }
json_data = json.dumps(output)
with os.fdopen(tmp_fd, 'w') as f: # Generate an ETag (hash of the response body)
f.write(output) etag_hash = hashlib.md5(json_data.encode('utf-8')).hexdigest()
response = make_response(send_file(path_or_file=tmp_file, # Create the response with ETag
mimetype='application/json; charset=UTF-8', response = Response(json_data, mimetype="application/json; charset=UTF-8")
etag=True)) response.set_etag(etag_hash)
# No longer needed
os.unlink(tmp_file)
return response return response

View File

@@ -1,14 +1,15 @@
#!/usr/bin/env python3
import os import os
import time import time
import re import re
from random import randint from random import randint
from loguru import logger from loguru import logger
from changedetectionio.content_fetchers.helpers import capture_stitched_together_full_page, SCREENSHOT_SIZE_STITCH_THRESHOLD
from changedetectionio.content_fetchers.base import manage_user_agent from changedetectionio.content_fetchers.base import manage_user_agent
from changedetectionio.safe_jinja import render as jinja_render from changedetectionio.safe_jinja import render as jinja_render
# Two flags, tell the JS which of the "Selector" or "Value" field should be enabled in the front end # Two flags, tell the JS which of the "Selector" or "Value" field should be enabled in the front end
# 0- off, 1- on # 0- off, 1- on
browser_step_ui_config = {'Choose one': '0 0', browser_step_ui_config = {'Choose one': '0 0',
@@ -31,6 +32,7 @@ browser_step_ui_config = {'Choose one': '0 0',
# 'Extract text and use as filter': '1 0', # 'Extract text and use as filter': '1 0',
'Goto site': '0 0', 'Goto site': '0 0',
'Goto URL': '0 1', 'Goto URL': '0 1',
'Make all child elements visible': '1 0',
'Press Enter': '0 0', 'Press Enter': '0 0',
'Select by label': '1 1', 'Select by label': '1 1',
'Scroll down': '0 0', 'Scroll down': '0 0',
@@ -38,6 +40,7 @@ browser_step_ui_config = {'Choose one': '0 0',
'Wait for seconds': '0 1', 'Wait for seconds': '0 1',
'Wait for text': '0 1', 'Wait for text': '0 1',
'Wait for text in element': '1 1', 'Wait for text in element': '1 1',
'Remove elements': '1 0',
# 'Press Page Down': '0 0', # 'Press Page Down': '0 0',
# 'Press Page Up': '0 0', # 'Press Page Up': '0 0',
# weird bug, come back to it later # weird bug, come back to it later
@@ -52,6 +55,8 @@ class steppable_browser_interface():
page = None page = None
start_url = None start_url = None
action_timeout = 10 * 1000
def __init__(self, start_url): def __init__(self, start_url):
self.start_url = start_url self.start_url = start_url
@@ -102,7 +107,7 @@ class steppable_browser_interface():
return return
elem = self.page.get_by_text(value) elem = self.page.get_by_text(value)
if elem.count(): if elem.count():
elem.first.click(delay=randint(200, 500), timeout=3000) elem.first.click(delay=randint(200, 500), timeout=self.action_timeout)
def action_click_element_containing_text_if_exists(self, selector=None, value=''): def action_click_element_containing_text_if_exists(self, selector=None, value=''):
logger.debug("Clicking element containing text if exists") logger.debug("Clicking element containing text if exists")
@@ -111,7 +116,7 @@ class steppable_browser_interface():
elem = self.page.get_by_text(value) elem = self.page.get_by_text(value)
logger.debug(f"Clicking element containing text - {elem.count()} elements found") logger.debug(f"Clicking element containing text - {elem.count()} elements found")
if elem.count(): if elem.count():
elem.first.click(delay=randint(200, 500), timeout=3000) elem.first.click(delay=randint(200, 500), timeout=self.action_timeout)
else: else:
return return
@@ -119,7 +124,7 @@ class steppable_browser_interface():
if not len(selector.strip()): if not len(selector.strip()):
return return
self.page.fill(selector, value, timeout=10 * 1000) self.page.fill(selector, value, timeout=self.action_timeout)
def action_execute_js(self, selector, value): def action_execute_js(self, selector, value):
response = self.page.evaluate(value) response = self.page.evaluate(value)
@@ -130,7 +135,7 @@ class steppable_browser_interface():
if not len(selector.strip()): if not len(selector.strip()):
return return
self.page.click(selector=selector, timeout=30 * 1000, delay=randint(200, 500)) self.page.click(selector=selector, timeout=self.action_timeout + 20 * 1000, delay=randint(200, 500))
def action_click_element_if_exists(self, selector, value): def action_click_element_if_exists(self, selector, value):
import playwright._impl._errors as _api_types import playwright._impl._errors as _api_types
@@ -138,7 +143,7 @@ class steppable_browser_interface():
if not len(selector.strip()): if not len(selector.strip()):
return return
try: try:
self.page.click(selector, timeout=10 * 1000, delay=randint(200, 500)) self.page.click(selector, timeout=self.action_timeout, delay=randint(200, 500))
except _api_types.TimeoutError as e: except _api_types.TimeoutError as e:
return return
except _api_types.Error as e: except _api_types.Error as e:
@@ -185,11 +190,29 @@ class steppable_browser_interface():
self.page.keyboard.press("PageDown", delay=randint(200, 500)) self.page.keyboard.press("PageDown", delay=randint(200, 500))
def action_check_checkbox(self, selector, value): def action_check_checkbox(self, selector, value):
self.page.locator(selector).check(timeout=1000) self.page.locator(selector).check(timeout=self.action_timeout)
def action_uncheck_checkbox(self, selector, value): def action_uncheck_checkbox(self, selector, value):
self.page.locator(selector, timeout=1000).uncheck(timeout=1000) self.page.locator(selector).uncheck(timeout=self.action_timeout)
def action_remove_elements(self, selector, value):
"""Removes all elements matching the given selector from the DOM."""
self.page.locator(selector).evaluate_all("els => els.forEach(el => el.remove())")
def action_make_all_child_elements_visible(self, selector, value):
"""Recursively makes all child elements inside the given selector fully visible."""
self.page.locator(selector).locator("*").evaluate_all("""
els => els.forEach(el => {
el.style.display = 'block'; // Forces it to be displayed
el.style.visibility = 'visible'; // Ensures it's not hidden
el.style.opacity = '1'; // Fully opaque
el.style.position = 'relative'; // Avoids 'absolute' hiding
el.style.height = 'auto'; // Expands collapsed elements
el.style.width = 'auto'; // Ensures full visibility
el.removeAttribute('hidden'); // Removes hidden attribute
el.classList.remove('hidden', 'd-none'); // Removes common CSS hidden classes
})
""")
# Responsible for maintaining a live 'context' with the chrome CDP # Responsible for maintaining a live 'context' with the chrome CDP
# @todo - how long do contexts live for anyway? # @todo - how long do contexts live for anyway?
@@ -257,6 +280,7 @@ class browsersteps_live_ui(steppable_browser_interface):
logger.debug(f"Time to browser setup {time.time()-now:.2f}s") logger.debug(f"Time to browser setup {time.time()-now:.2f}s")
self.page.wait_for_timeout(1 * 1000) self.page.wait_for_timeout(1 * 1000)
def mark_as_closed(self): def mark_as_closed(self):
logger.debug("Page closed, cleaning up..") logger.debug("Page closed, cleaning up..")
@@ -274,39 +298,30 @@ class browsersteps_live_ui(steppable_browser_interface):
now = time.time() now = time.time()
self.page.wait_for_timeout(1 * 1000) self.page.wait_for_timeout(1 * 1000)
# The actual screenshot
screenshot = self.page.screenshot(type='jpeg', full_page=True, quality=40)
full_height = self.page.evaluate("document.documentElement.scrollHeight")
if full_height >= SCREENSHOT_SIZE_STITCH_THRESHOLD:
logger.warning(f"Page full Height: {full_height}px longer than {SCREENSHOT_SIZE_STITCH_THRESHOLD}px, using 'stitched screenshot method'.")
screenshot = capture_stitched_together_full_page(self.page)
else:
screenshot = self.page.screenshot(type='jpeg', full_page=True, quality=40)
logger.debug(f"Time to get screenshot from browser {time.time() - now:.2f}s")
now = time.time()
self.page.evaluate("var include_filters=''") self.page.evaluate("var include_filters=''")
# Go find the interactive elements # Go find the interactive elements
# @todo in the future, something smarter that can scan for elements with .click/focus etc event handlers? # @todo in the future, something smarter that can scan for elements with .click/focus etc event handlers?
elements = 'a,button,input,select,textarea,i,th,td,p,li,h1,h2,h3,h4,div,span' elements = 'a,button,input,select,textarea,i,th,td,p,li,h1,h2,h3,h4,div,span'
xpath_element_js = xpath_element_js.replace('%ELEMENTS%', elements) xpath_element_js = xpath_element_js.replace('%ELEMENTS%', elements)
xpath_data = self.page.evaluate("async () => {" + xpath_element_js + "}") xpath_data = self.page.evaluate("async () => {" + xpath_element_js + "}")
# So the JS will find the smallest one first # So the JS will find the smallest one first
xpath_data['size_pos'] = sorted(xpath_data['size_pos'], key=lambda k: k['width'] * k['height'], reverse=True) xpath_data['size_pos'] = sorted(xpath_data['size_pos'], key=lambda k: k['width'] * k['height'], reverse=True)
logger.debug(f"Time to complete get_current_state of browser {time.time()-now:.2f}s") logger.debug(f"Time to scrape xpath element data in browser {time.time()-now:.2f}s")
# except
# playwright._impl._api_types.Error: Browser closed. # playwright._impl._api_types.Error: Browser closed.
# @todo show some countdown timer? # @todo show some countdown timer?
return (screenshot, xpath_data) return (screenshot, xpath_data)
def request_visualselector_data(self):
"""
Does the same that the playwright operation in content_fetcher does
This is used to just bump the VisualSelector data so it' ready to go if they click on the tab
@todo refactor and remove duplicate code, add include_filters
:param xpath_data:
:param screenshot:
:param current_include_filters:
:return:
"""
import importlib.resources
self.page.evaluate("var include_filters=''")
xpath_element_js = importlib.resources.files("changedetectionio.content_fetchers.res").joinpath('xpath_element_scraper.js').read_text()
from changedetectionio.content_fetchers import visualselector_xpath_selectors
xpath_element_js = xpath_element_js.replace('%ELEMENTS%', visualselector_xpath_selectors)
xpath_data = self.page.evaluate("async () => {" + xpath_element_js + "}")
screenshot = self.page.screenshot(type='jpeg', full_page=True, quality=int(os.getenv("SCREENSHOT_QUALITY", 72)))
return (screenshot, xpath_data)

View File

@@ -0,0 +1,74 @@
from flask import Blueprint, request, redirect, url_for, flash, render_template
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.auth_decorator import login_optionally_required
from changedetectionio.blueprint.imports.importer import (
import_url_list,
import_distill_io_json,
import_xlsx_wachete,
import_xlsx_custom
)
def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData):
import_blueprint = Blueprint('imports', __name__, template_folder="templates")
@import_blueprint.route("/import", methods=['GET', 'POST'])
@login_optionally_required
def import_page():
remaining_urls = []
from changedetectionio import forms
if request.method == 'POST':
# URL List import
if request.values.get('urls') and len(request.values.get('urls').strip()):
# Import and push into the queue for immediate update check
importer_handler = import_url_list()
importer_handler.run(data=request.values.get('urls'), flash=flash, datastore=datastore, processor=request.values.get('processor', 'text_json_diff'))
for uuid in importer_handler.new_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
if len(importer_handler.remaining_data) == 0:
return redirect(url_for('index'))
else:
remaining_urls = importer_handler.remaining_data
# Distill.io import
if request.values.get('distill-io') and len(request.values.get('distill-io').strip()):
# Import and push into the queue for immediate update check
d_importer = import_distill_io_json()
d_importer.run(data=request.values.get('distill-io'), flash=flash, datastore=datastore)
for uuid in d_importer.new_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
# XLSX importer
if request.files and request.files.get('xlsx_file'):
file = request.files['xlsx_file']
if request.values.get('file_mapping') == 'wachete':
w_importer = import_xlsx_wachete()
w_importer.run(data=file, flash=flash, datastore=datastore)
else:
w_importer = import_xlsx_custom()
# Building mapping of col # to col # type
map = {}
for i in range(10):
c = request.values.get(f"custom_xlsx[col_{i}]")
v = request.values.get(f"custom_xlsx[col_type_{i}]")
if c and v:
map[int(c)] = v
w_importer.import_profile = map
w_importer.run(data=file, flash=flash, datastore=datastore)
for uuid in w_importer.new_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
# Could be some remaining, or we could be on GET
form = forms.importForm(formdata=request.form if request.method == 'POST' else None)
output = render_template("import.html",
form=form,
import_url_list_remaining="\n".join(remaining_urls),
original_distill_json=''
)
return output
return import_blueprint

View File

@@ -1,6 +1,5 @@
from abc import ABC, abstractmethod from abc import abstractmethod
import time import time
import validators
from wtforms import ValidationError from wtforms import ValidationError
from loguru import logger from loguru import logger
@@ -241,7 +240,7 @@ class import_xlsx_custom(Importer):
return return
# @todo cehck atleast 2 rows, same in other method # @todo cehck atleast 2 rows, same in other method
from .forms import validate_url from changedetectionio.forms import validate_url
row_i = 1 row_i = 1
try: try:
@@ -300,4 +299,4 @@ class import_xlsx_custom(Importer):
row_i += 1 row_i += 1
flash( flash(
"{} imported from custom .xlsx in {:.2f}s".format(len(self.new_uuids), time.time() - now)) "{} imported from custom .xlsx in {:.2f}s".format(len(self.new_uuids), time.time() - now))

View File

@@ -13,29 +13,27 @@
</div> </div>
<div class="box-wrap inner"> <div class="box-wrap inner">
<form class="pure-form" action="{{url_for('import_page')}}" method="POST" enctype="multipart/form-data"> <form class="pure-form" action="{{url_for('imports.import_page')}}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="tab-pane-inner" id="url-list"> <div class="tab-pane-inner" id="url-list">
<legend> <div class="pure-control-group">
Enter one URL per line, and optionally add tags for each URL after a space, delineated by comma Enter one URL per line, and optionally add tags for each URL after a space, delineated by comma
(,): (,):
<br> <br>
<code>https://example.com tag1, tag2, last tag</code> <p><strong>Example: </strong><code>https://example.com tag1, tag2, last tag</code></p>
<br>
URLs which do not pass validation will stay in the textarea. URLs which do not pass validation will stay in the textarea.
</legend> </div>
{{ render_field(form.processor, class="processor") }} {{ render_field(form.processor, class="processor") }}
<div class="pure-control-group">
<textarea name="urls" class="pure-input-1-2" placeholder="https://" <textarea name="urls" class="pure-input-1-2" placeholder="https://"
style="width: 100%; style="width: 100%;
font-family:monospace; font-family:monospace;
white-space: pre; white-space: pre;
overflow-wrap: normal; overflow-wrap: normal;
overflow-x: scroll;" rows="25">{{ import_url_list_remaining }}</textarea> overflow-x: scroll;" rows="25">{{ import_url_list_remaining }}</textarea>
</div>
<div id="quick-watch-processor-type"> <div id="quick-watch-processor-type"></div>
</div>
</div> </div>
@@ -43,7 +41,7 @@
<legend> <div class="pure-control-group">
Copy and Paste your Distill.io watch 'export' file, this should be a JSON file.<br> Copy and Paste your Distill.io watch 'export' file, this should be a JSON file.<br>
This is <i>experimental</i>, supported fields are <code>name</code>, <code>uri</code>, <code>tags</code>, <code>config:selections</code>, the rest (including <code>schedule</code>) are ignored. This is <i>experimental</i>, supported fields are <code>name</code>, <code>uri</code>, <code>tags</code>, <code>config:selections</code>, the rest (including <code>schedule</code>) are ignored.
<br> <br>
@@ -51,7 +49,7 @@
How to export? <a href="https://distill.io/docs/web-monitor/how-export-and-import-monitors/">https://distill.io/docs/web-monitor/how-export-and-import-monitors/</a><br> How to export? <a href="https://distill.io/docs/web-monitor/how-export-and-import-monitors/">https://distill.io/docs/web-monitor/how-export-and-import-monitors/</a><br>
Be sure to set your default fetcher to Chrome if required.<br> Be sure to set your default fetcher to Chrome if required.<br>
</p> </p>
</legend> </div>
<textarea name="distill-io" class="pure-input-1-2" style="width: 100%; <textarea name="distill-io" class="pure-input-1-2" style="width: 100%;
@@ -122,4 +120,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,103 @@
import time
import datetime
import pytz
from flask import Blueprint, make_response, request, url_for
from loguru import logger
from feedgen.feed import FeedGenerator
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.safe_jinja import render as jinja_render
def construct_blueprint(datastore: ChangeDetectionStore):
rss_blueprint = Blueprint('rss', __name__)
# Import the login decorator if needed
# from changedetectionio.auth_decorator import login_optionally_required
@rss_blueprint.route("/", methods=['GET'])
def feed():
now = time.time()
# Always requires token set
app_rss_token = datastore.data['settings']['application'].get('rss_access_token')
rss_url_token = request.args.get('token')
if rss_url_token != app_rss_token:
return "Access denied, bad token", 403
from changedetectionio import diff
limit_tag = request.args.get('tag', '').lower().strip()
# Be sure limit_tag is a uuid
for uuid, tag in datastore.data['settings']['application'].get('tags', {}).items():
if limit_tag == tag.get('title', '').lower().strip():
limit_tag = uuid
# Sort by last_changed and add the uuid which is usually the key..
sorted_watches = []
# @todo needs a .itemsWithTag() or something - then we can use that in Jinaj2 and throw this away
for uuid, watch in datastore.data['watching'].items():
# @todo tag notification_muted skip also (improve Watch model)
if datastore.data['settings']['application'].get('rss_hide_muted_watches') and watch.get('notification_muted'):
continue
if limit_tag and not limit_tag in watch['tags']:
continue
watch['uuid'] = uuid
sorted_watches.append(watch)
sorted_watches.sort(key=lambda x: x.last_changed, reverse=False)
fg = FeedGenerator()
fg.title('changedetection.io')
fg.description('Feed description')
fg.link(href='https://changedetection.io')
for watch in sorted_watches:
dates = list(watch.history.keys())
# Re #521 - Don't bother processing this one if theres less than 2 snapshots, means we never had a change detected.
if len(dates) < 2:
continue
if not watch.viewed:
# Re #239 - GUID needs to be individual for each event
# @todo In the future make this a configurable link back (see work on BASE_URL https://github.com/dgtlmoon/changedetection.io/pull/228)
guid = "{}/{}".format(watch['uuid'], watch.last_changed)
fe = fg.add_entry()
# 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
# Dict val base_url will get overriden with the env var if it is set.
ext_base_url = datastore.data['settings']['application'].get('active_base_url')
# Because we are called via whatever web server, flask should figure out the right path (
diff_link = {'href': url_for('ui.ui_views.diff_history_page', uuid=watch['uuid'], _external=True)}
fe.link(link=diff_link)
# @todo watch should be a getter - watch.get('title') (internally if URL else..)
watch_title = watch.get('title') if watch.get('title') else watch.get('url')
fe.title(title=watch_title)
html_diff = diff.render_diff(previous_version_file_contents=watch.get_history_snapshot(dates[-2]),
newest_version_file_contents=watch.get_history_snapshot(dates[-1]),
include_equal=False,
line_feed_sep="<br>")
# @todo Make this configurable and also consider html-colored markup
# @todo User could decide if <link> goes to the diff page, or to the watch link
rss_template = "<html><body>\n<h4><a href=\"{{watch_url}}\">{{watch_title}}</a></h4>\n<p>{{html_diff}}</p>\n</body></html>\n"
content = jinja_render(template_str=rss_template, watch_title=watch_title, html_diff=html_diff, watch_url=watch.link)
fe.content(content=content, type='CDATA')
fe.guid(guid, permalink=False)
dt = datetime.datetime.fromtimestamp(int(watch.newest_history_key))
dt = dt.replace(tzinfo=pytz.UTC)
fe.pubDate(dt)
response = make_response(fg.rss_str())
response.headers.set('Content-Type', 'application/rss+xml;charset=utf-8')
logger.trace(f"RSS generated in {time.time() - now:.3f}s")
return response
return rss_blueprint

View File

@@ -0,0 +1,120 @@
import os
from copy import deepcopy
from datetime import datetime
from zoneinfo import ZoneInfo, available_timezones
import secrets
import flask_login
from flask import Blueprint, render_template, request, redirect, url_for, flash
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.auth_decorator import login_optionally_required
def construct_blueprint(datastore: ChangeDetectionStore):
settings_blueprint = Blueprint('settings', __name__, template_folder="templates")
@settings_blueprint.route("/", methods=['GET', "POST"])
@login_optionally_required
def settings_page():
from changedetectionio import forms
default = deepcopy(datastore.data['settings'])
if datastore.proxy_list is not None:
available_proxies = list(datastore.proxy_list.keys())
# When enabled
system_proxy = datastore.data['settings']['requests']['proxy']
# In the case it doesnt exist anymore
if not system_proxy in available_proxies:
system_proxy = None
default['requests']['proxy'] = system_proxy if system_proxy is not None else available_proxies[0]
# Used by the form handler to keep or remove the proxy settings
default['proxy_list'] = available_proxies[0]
# 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=default,
extra_notification_tokens=datastore.get_unique_notification_tokens_available()
)
# Remove the last option 'System default'
form.application.form.notification_format.choices.pop()
if datastore.proxy_list is None:
# @todo - Couldn't get setattr() etc dynamic addition working, so remove it instead
del form.requests.form.proxy
else:
form.requests.form.proxy.choices = []
for p in datastore.proxy_list:
form.requests.form.proxy.choices.append(tuple((p, datastore.proxy_list[p]['label'])))
if request.method == 'POST':
# Password unset is a GET, but we can lock the session to a salted env password to always need the password
if form.application.form.data.get('removepassword_button', False):
# SALTED_PASS means the password is "locked" to what we set in the Env var
if not os.getenv("SALTED_PASS", False):
datastore.remove_password()
flash("Password protection removed.", 'notice')
flask_login.logout_user()
return redirect(url_for('settings.settings_page'))
if form.validate():
# Don't set password to False when a password is set - should be only removed with the `removepassword` button
app_update = dict(deepcopy(form.data['application']))
# Never update password with '' or False (Added by wtforms when not in submission)
if 'password' in app_update and not app_update['password']:
del (app_update['password'])
datastore.data['settings']['application'].update(app_update)
datastore.data['settings']['requests'].update(form.data['requests'])
if not os.getenv("SALTED_PASS", False) and len(form.application.form.password.encrypted_password):
datastore.data['settings']['application']['password'] = form.application.form.password.encrypted_password
datastore.needs_write_urgent = True
flash("Password protection enabled.", 'notice')
flask_login.logout_user()
return redirect(url_for('index'))
datastore.needs_write_urgent = True
flash("Settings updated.")
else:
flash("An error occurred, please see below.", "error")
# Convert to ISO 8601 format, all date/time relative events stored as UTC time
utc_time = datetime.now(ZoneInfo("UTC")).isoformat()
output = render_template("settings.html",
api_key=datastore.data['settings']['application'].get('api_access_token'),
available_timezones=sorted(available_timezones()),
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
extra_notification_token_placeholder_info=datastore.get_unique_notification_token_placeholders_available(),
form=form,
hide_remove_pass=os.getenv("SALTED_PASS", False),
min_system_recheck_seconds=int(os.getenv('MINIMUM_SECONDS_RECHECK_TIME', 3)),
settings_application=datastore.data['settings']['application'],
timezone_default_config=datastore.data['settings']['application'].get('timezone'),
utc_time=utc_time,
)
return output
@settings_blueprint.route("/reset-api-key", methods=['GET'])
@login_optionally_required
def settings_reset_api_key():
secret = secrets.token_hex(16)
datastore.data['settings']['application']['api_access_token'] = secret
datastore.needs_write_urgent = True
flash("API Key was regenerated.")
return redirect(url_for('settings.settings_page')+'#api')
@settings_blueprint.route("/notification-logs", methods=['GET'])
@login_optionally_required
def notification_logs():
from changedetectionio.flask_app import notification_debug_log
output = render_template("notification-log.html",
logs=notification_debug_log if len(notification_debug_log) else ["Notification logs are empty - no notifications sent yet."])
return output
return settings_blueprint

View File

@@ -4,7 +4,7 @@
{% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form %} {% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form %}
{% from '_common_fields.html' import render_common_settings_form %} {% from '_common_fields.html' import render_common_settings_form %}
<script> <script>
const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="global-settings")}}"; const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', mode="global-settings")}}";
{% if emailprefix %} {% if emailprefix %}
const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}'); const email_notification_prefix=JSON.parse('{{emailprefix|tojson}}');
{% endif %} {% endif %}
@@ -28,7 +28,7 @@
</ul> </ul>
</div> </div>
<div class="box-wrap inner"> <div class="box-wrap inner">
<form class="pure-form pure-form-stacked settings" action="{{url_for('settings_page')}}" method="POST"> <form class="pure-form pure-form-stacked settings" action="{{url_for('settings.settings_page')}}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" > <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >
<div class="tab-pane-inner" id="general"> <div class="tab-pane-inner" id="general">
<fieldset> <fieldset>
@@ -203,7 +203,7 @@ nav
</div> </div>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<a href="{{url_for('settings_reset_api_key')}}" class="pure-button button-small button-cancel">Regenerate API key</a> <a href="{{url_for('settings.settings_reset_api_key')}}" class="pure-button button-small button-cancel">Regenerate API key</a>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<h4>Chrome Extension</h4> <h4>Chrome Extension</h4>
@@ -214,7 +214,7 @@ nav
<a id="chrome-extension-link" <a id="chrome-extension-link"
title="Try our new Chrome Extension!" title="Try our new Chrome Extension!"
href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop"> href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop">
<img src="{{ url_for('static_content', group='images', filename='Google-Chrome-icon.png') }}" alt="Chrome"> <img alt="Chrome store icon" src="{{ url_for('static_content', group='images', filename='Google-Chrome-icon.png') }}" alt="Chrome">
Chrome Webstore Chrome Webstore
</a> </a>
</p> </p>
@@ -280,9 +280,7 @@ nav
</div> </div>
</div> </div>
<p>
Your proxy provider may need to whitelist our IP of <code>204.15.192.195</code>
</p>
<p><strong>Tip</strong>: "Residential" and "Mobile" proxy type can be more successfull than "Data Center" for blocked websites. <p><strong>Tip</strong>: "Residential" and "Mobile" proxy type can be more successfull than "Data Center" for blocked websites.
<div class="pure-control-group" id="extra-proxies-setting"> <div class="pure-control-group" id="extra-proxies-setting">
@@ -302,7 +300,7 @@ nav
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_button(form.save_button) }} {{ render_button(form.save_button) }}
<a href="{{url_for('index')}}" class="pure-button button-small button-cancel">Back</a> <a href="{{url_for('index')}}" class="pure-button button-small button-cancel">Back</a>
<a href="{{url_for('clear_all_history')}}" class="pure-button button-small button-error">Clear Snapshot History</a> <a href="{{url_for('ui.clear_all_history')}}" class="pure-button button-small button-error">Clear Snapshot History</a>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -3,7 +3,7 @@
{% from '_helpers.html' import render_field, render_checkbox_field, render_button %} {% from '_helpers.html' import render_field, render_checkbox_field, render_button %}
{% from '_common_fields.html' import render_common_settings_form %} {% from '_common_fields.html' import render_common_settings_form %}
<script> <script>
const notification_base_url="{{url_for('ajax_callback_send_notification_test', mode="group-settings")}}"; const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', mode="group-settings")}}";
</script> </script>
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
@@ -124,7 +124,7 @@ nav
{% if has_default_notification_urls %} {% if has_default_notification_urls %}
<div class="inline-warning"> <div class="inline-warning">
<img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="Look out!" title="Lookout!" > <img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="Look out!" title="Lookout!" >
There are <a href="{{ url_for('settings_page')}}#notifications">system-wide notification URLs enabled</a>, this form will override notification settings for this watch only &dash; an empty Notification URL list here will still send notifications. There are <a href="{{ url_for('settings.settings_page')}}#notifications">system-wide notification URLs enabled</a>, this form will override notification settings for this watch only &dash; an empty Notification URL list here will still send notifications.
</div> </div>
{% endif %} {% endif %}
<a href="#notifications" id="notification-setting-reset-to-default" class="pure-button button-xsmall" style="right: 20px; top: 20px; position: absolute; background-color: #5f42dd; border-radius: 4px; font-size: 70%; color: #fff">Use system defaults</a> <a href="#notifications" id="notification-setting-reset-to-default" class="pure-button button-xsmall" style="right: 20px; top: 20px; position: absolute; background-color: #5f42dd; border-radius: 4px; font-size: 70%; color: #fff">Use system defaults</a>

View File

@@ -0,0 +1,301 @@
import time
from flask import Blueprint, request, redirect, url_for, flash, render_template, session
from loguru import logger
from functools import wraps
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.blueprint.ui.edit import construct_blueprint as construct_edit_blueprint
from changedetectionio.blueprint.ui.notification import construct_blueprint as construct_notification_blueprint
from changedetectionio.blueprint.ui.views import construct_blueprint as construct_views_blueprint
def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_update_threads, queuedWatchMetaData):
ui_blueprint = Blueprint('ui', __name__, template_folder="templates")
# Register the edit blueprint
edit_blueprint = construct_edit_blueprint(datastore, update_q, queuedWatchMetaData)
ui_blueprint.register_blueprint(edit_blueprint)
# Register the notification blueprint
notification_blueprint = construct_notification_blueprint(datastore)
ui_blueprint.register_blueprint(notification_blueprint)
# Register the views blueprint
views_blueprint = construct_views_blueprint(datastore, update_q, queuedWatchMetaData)
ui_blueprint.register_blueprint(views_blueprint)
# Import the login decorator
from changedetectionio.auth_decorator import login_optionally_required
@ui_blueprint.route("/clear_history/<string:uuid>", methods=['GET'])
@login_optionally_required
def clear_watch_history(uuid):
try:
datastore.clear_watch_history(uuid)
except KeyError:
flash('Watch not found', 'error')
else:
flash("Cleared snapshot history for watch {}".format(uuid))
return redirect(url_for('index'))
@ui_blueprint.route("/clear_history", methods=['GET', 'POST'])
@login_optionally_required
def clear_all_history():
if request.method == 'POST':
confirmtext = request.form.get('confirmtext')
if confirmtext == 'clear':
for uuid in datastore.data['watching'].keys():
datastore.clear_watch_history(uuid)
flash("Cleared snapshot history for all watches")
else:
flash('Incorrect confirmation text.', 'error')
return redirect(url_for('index'))
output = render_template("clear_all_history.html")
return output
# Clear all statuses, so we do not see the 'unviewed' class
@ui_blueprint.route("/form/mark-all-viewed", methods=['GET'])
@login_optionally_required
def mark_all_viewed():
# Save the current newest history as the most recently viewed
with_errors = request.args.get('with_errors') == "1"
for watch_uuid, watch in datastore.data['watching'].items():
if with_errors and not watch.get('last_error'):
continue
datastore.set_last_viewed(watch_uuid, int(time.time()))
return redirect(url_for('index'))
@ui_blueprint.route("/delete", methods=['GET'])
@login_optionally_required
def form_delete():
uuid = request.args.get('uuid')
if uuid != 'all' and not uuid in datastore.data['watching'].keys():
flash('The watch by UUID {} does not exist.'.format(uuid), 'error')
return redirect(url_for('index'))
# More for testing, possible to return the first/only
if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop()
datastore.delete(uuid)
flash('Deleted.')
return redirect(url_for('index'))
@ui_blueprint.route("/clone", methods=['GET'])
@login_optionally_required
def form_clone():
uuid = request.args.get('uuid')
# More for testing, possible to return the first/only
if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop()
new_uuid = datastore.clone(uuid)
if new_uuid:
if not datastore.data['watching'].get(uuid).get('paused'):
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=5, item={'uuid': new_uuid}))
flash('Cloned.')
return redirect(url_for('index'))
@ui_blueprint.route("/checknow", methods=['GET'])
@login_optionally_required
def form_watch_checknow():
# Forced recheck will skip the 'skip if content is the same' rule (, 'reprocess_existing_data': True})))
tag = request.args.get('tag')
uuid = request.args.get('uuid')
with_errors = request.args.get('with_errors') == "1"
i = 0
running_uuids = []
for t in running_update_threads:
running_uuids.append(t.current_uuid)
if uuid:
if uuid not in running_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
i += 1
else:
# Recheck all, including muted
for watch_uuid, watch in datastore.data['watching'].items():
if not watch['paused']:
if watch_uuid not in running_uuids:
if with_errors and not watch.get('last_error'):
continue
if tag != None and tag not in watch['tags']:
continue
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid}))
i += 1
if i == 1:
flash("Queued 1 watch for rechecking.")
if i > 1:
flash("Queued {} watches for rechecking.".format(i))
if i == 0:
flash("No watches available to recheck.")
return redirect(url_for('index'))
@ui_blueprint.route("/form/checkbox-operations", methods=['POST'])
@login_optionally_required
def form_watch_list_checkbox_operations():
op = request.form['op']
uuids = request.form.getlist('uuids')
if (op == 'delete'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.delete(uuid.strip())
flash("{} watches deleted".format(len(uuids)))
elif (op == 'pause'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.data['watching'][uuid.strip()]['paused'] = True
flash("{} watches paused".format(len(uuids)))
elif (op == 'unpause'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.data['watching'][uuid.strip()]['paused'] = False
flash("{} watches unpaused".format(len(uuids)))
elif (op == 'mark-viewed'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.set_last_viewed(uuid, int(time.time()))
flash("{} watches updated".format(len(uuids)))
elif (op == 'mute'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.data['watching'][uuid.strip()]['notification_muted'] = True
flash("{} watches muted".format(len(uuids)))
elif (op == 'unmute'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.data['watching'][uuid.strip()]['notification_muted'] = False
flash("{} watches un-muted".format(len(uuids)))
elif (op == 'recheck'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
# Recheck and require a full reprocessing
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
flash("{} watches queued for rechecking".format(len(uuids)))
elif (op == 'clear-errors'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.data['watching'][uuid]["last_error"] = False
flash(f"{len(uuids)} watches errors cleared")
elif (op == 'clear-history'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.clear_watch_history(uuid)
flash("{} watches cleared/reset.".format(len(uuids)))
elif (op == 'notification-default'):
from changedetectionio.notification import (
default_notification_format_for_watch
)
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
datastore.data['watching'][uuid.strip()]['notification_title'] = None
datastore.data['watching'][uuid.strip()]['notification_body'] = None
datastore.data['watching'][uuid.strip()]['notification_urls'] = []
datastore.data['watching'][uuid.strip()]['notification_format'] = default_notification_format_for_watch
flash("{} watches set to use default notification settings".format(len(uuids)))
elif (op == 'assign-tag'):
op_extradata = request.form.get('op_extradata', '').strip()
if op_extradata:
tag_uuid = datastore.add_tag(name=op_extradata)
if op_extradata and tag_uuid:
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
# Bug in old versions caused by bad edit page/tag handler
if isinstance(datastore.data['watching'][uuid]['tags'], str):
datastore.data['watching'][uuid]['tags'] = []
datastore.data['watching'][uuid]['tags'].append(tag_uuid)
flash(f"{len(uuids)} watches were tagged")
return redirect(url_for('index'))
@ui_blueprint.route("/share-url/<string:uuid>", methods=['GET'])
@login_optionally_required
def form_share_put_watch(uuid):
"""Given a watch UUID, upload the info and return a share-link
the share-link can be imported/added"""
import requests
import json
from copy import deepcopy
# more for testing
if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop()
# copy it to memory as trim off what we dont need (history)
watch = deepcopy(datastore.data['watching'].get(uuid))
# For older versions that are not a @property
if (watch.get('history')):
del (watch['history'])
# for safety/privacy
for k in list(watch.keys()):
if k.startswith('notification_'):
del watch[k]
for r in['uuid', 'last_checked', 'last_changed']:
if watch.get(r):
del (watch[r])
# Add the global stuff which may have an impact
watch['ignore_text'] += datastore.data['settings']['application']['global_ignore_text']
watch['subtractive_selectors'] += datastore.data['settings']['application']['global_subtractive_selectors']
watch_json = json.dumps(watch)
try:
r = requests.request(method="POST",
data={'watch': watch_json},
url="https://changedetection.io/share/share",
headers={'App-Guid': datastore.data['app_guid']})
res = r.json()
# Add to the flask session
session['share-link'] = f"https://changedetection.io/share/{res['share_key']}"
except Exception as e:
logger.error(f"Error sharing -{str(e)}")
flash(f"Could not share, something went wrong while communicating with the share server - {str(e)}", 'error')
return redirect(url_for('index'))
return ui_blueprint

View File

@@ -0,0 +1,333 @@
import time
from copy import deepcopy
import os
import importlib.resources
from flask import Blueprint, request, redirect, url_for, flash, render_template, make_response, send_from_directory, abort
from loguru import logger
from jinja2 import Environment, FileSystemLoader
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.auth_decorator import login_optionally_required
from changedetectionio.time_handler import is_within_schedule
def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData):
edit_blueprint = Blueprint('ui_edit', __name__, template_folder="../ui/templates")
def _watch_has_tag_options_set(watch):
"""This should be fixed better so that Tag is some proper Model, a tag is just a Watch also"""
for tag_uuid, tag in datastore.data['settings']['application'].get('tags', {}).items():
if tag_uuid in watch.get('tags', []) and (tag.get('include_filters') or tag.get('subtractive_selectors')):
return True
@edit_blueprint.route("/edit/<string:uuid>", methods=['GET', 'POST'])
@login_optionally_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):
from changedetectionio import forms
from changedetectionio.blueprint.browser_steps.browser_steps import browser_step_ui_config
from changedetectionio import processors
import importlib
# 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':
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'))
switch_processor = request.args.get('switch_processor')
if switch_processor:
for p in processors.available_processors():
if p[0] == switch_processor:
datastore.data['watching'][uuid]['processor'] = switch_processor
flash(f"Switched to mode - {p[1]}.")
datastore.clear_watch_history(uuid)
redirect(url_for('ui_edit.edit_page', uuid=uuid))
# be sure we update with a copy instead of accidently editing the live object by reference
default = deepcopy(datastore.data['watching'][uuid])
# Defaults for proxy choice
if datastore.proxy_list is not None: # When enabled
# @todo
# Radio needs '' not None, or incase that the chosen one no longer exists
if default['proxy'] is None or not any(default['proxy'] in tup for tup in datastore.proxy_list):
default['proxy'] = ''
# proxy_override set to the json/text list of the items
# Does it use some custom form? does one exist?
processor_name = datastore.data['watching'][uuid].get('processor', '')
processor_classes = next((tpl for tpl in processors.find_processors() if tpl[1] == processor_name), None)
if not processor_classes:
flash(f"Cannot load the edit form for processor/plugin '{processor_classes[1]}', plugin missing?", 'error')
return redirect(url_for('index'))
parent_module = processors.get_parent_module(processor_classes[0])
try:
# Get the parent of the "processor.py" go up one, get the form (kinda spaghetti but its reusing existing code)
forms_module = importlib.import_module(f"{parent_module.__name__}.forms")
# Access the 'processor_settings_form' class from the 'forms' module
form_class = getattr(forms_module, 'processor_settings_form')
except ModuleNotFoundError as e:
# .forms didnt exist
form_class = forms.processor_text_json_diff_form
except AttributeError as e:
# .forms exists but no useful form
form_class = forms.processor_text_json_diff_form
form = form_class(formdata=request.form if request.method == 'POST' else None,
data=default,
extra_notification_tokens=default.extra_notification_token_values(),
default_system_settings=datastore.data['settings']
)
# For the form widget tag UUID back to "string name" for the field
form.tags.datastore = datastore
# Used by some forms that need to dig deeper
form.datastore = datastore
form.watch = default
for p in datastore.extra_browsers:
form.fetch_backend.choices.append(p)
form.fetch_backend.choices.append(("system", 'System settings default'))
# form.browser_steps[0] can be assumed that we 'goto url' first
if datastore.proxy_list is None:
# @todo - Couldn't get setattr() etc dynamic addition working, so remove it instead
del form.proxy
else:
form.proxy.choices = [('', 'Default')]
for p in datastore.proxy_list:
form.proxy.choices.append(tuple((p, datastore.proxy_list[p]['label'])))
if request.method == 'POST' and form.validate():
# If they changed processor, it makes sense to reset it.
if datastore.data['watching'][uuid].get('processor') != form.data.get('processor'):
datastore.data['watching'][uuid].clear_watch()
flash("Reset watch history due to change of processor")
extra_update_obj = {
'consecutive_filter_failures': 0,
'last_error' : False
}
if request.args.get('unpause_on_save'):
extra_update_obj['paused'] = False
extra_update_obj['time_between_check'] = form.time_between_check.data
# Ignore text
form_ignore_text = form.ignore_text.data
datastore.data['watching'][uuid]['ignore_text'] = form_ignore_text
# Be sure proxy value is None
if datastore.proxy_list is not None and form.data['proxy'] == '':
extra_update_obj['proxy'] = None
# Unsetting all filter_text methods should make it go back to default
# This particularly affects tests running
if 'filter_text_added' in form.data and not form.data.get('filter_text_added') \
and 'filter_text_replaced' in form.data and not form.data.get('filter_text_replaced') \
and 'filter_text_removed' in form.data and not form.data.get('filter_text_removed'):
extra_update_obj['filter_text_added'] = True
extra_update_obj['filter_text_replaced'] = True
extra_update_obj['filter_text_removed'] = True
# Because wtforms doesn't support accessing other data in process_ , but we convert the CSV list of tags back to a list of UUIDs
tag_uuids = []
if form.data.get('tags'):
# Sometimes in testing this can be list, dont know why
if type(form.data.get('tags')) == list:
extra_update_obj['tags'] = form.data.get('tags')
else:
for t in form.data.get('tags').split(','):
tag_uuids.append(datastore.add_tag(name=t))
extra_update_obj['tags'] = tag_uuids
datastore.data['watching'][uuid].update(form.data)
datastore.data['watching'][uuid].update(extra_update_obj)
if not datastore.data['watching'][uuid].get('tags'):
# Force it to be a list, because form.data['tags'] will be string if nothing found
# And del(form.data['tags'] ) wont work either for some reason
datastore.data['watching'][uuid]['tags'] = []
# Recast it if need be to right data Watch handler
watch_class = processors.get_custom_watch_obj_for_processor(form.data.get('processor'))
datastore.data['watching'][uuid] = watch_class(datastore_path=datastore.datastore_path, default=datastore.data['watching'][uuid])
flash("Updated watch - unpaused!" if request.args.get('unpause_on_save') else "Updated watch.")
# Re #286 - We wait for syncing new data to disk in another thread every 60 seconds
# But in the case something is added we should save straight away
datastore.needs_write_urgent = True
# Do not queue on edit if its not within the time range
# @todo maybe it should never queue anyway on edit...
is_in_schedule = True
watch = datastore.data['watching'].get(uuid)
if watch.get('time_between_check_use_default'):
time_schedule_limit = datastore.data['settings']['requests'].get('time_schedule_limit', {})
else:
time_schedule_limit = watch.get('time_schedule_limit')
tz_name = time_schedule_limit.get('timezone')
if not tz_name:
tz_name = datastore.data['settings']['application'].get('timezone', 'UTC')
if time_schedule_limit and time_schedule_limit.get('enabled'):
try:
is_in_schedule = is_within_schedule(time_schedule_limit=time_schedule_limit,
default_tz=tz_name
)
except Exception as e:
logger.error(
f"{uuid} - Recheck scheduler, error handling timezone, check skipped - TZ name '{tz_name}' - {str(e)}")
return False
#############################
if not datastore.data['watching'][uuid].get('paused') and is_in_schedule:
# Queue the watch for immediate recheck, with a higher priority
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
# Diff page [edit] link should go back to diff page
if request.args.get("next") and request.args.get("next") == 'diff':
return redirect(url_for('ui.ui_views.diff_history_page', uuid=uuid))
return redirect(url_for('index', tag=request.args.get("tag",'')))
else:
if request.method == 'POST' and not form.validate():
flash("An error occurred, please see below.", "error")
visualselector_data_is_ready = datastore.visualselector_data_is_ready(uuid)
# JQ is difficult to install on windows and must be manually added (outside requirements.txt)
jq_support = True
try:
import jq
except ModuleNotFoundError:
jq_support = False
watch = datastore.data['watching'].get(uuid)
system_uses_webdriver = datastore.data['settings']['application']['fetch_backend'] == 'html_webdriver'
watch_uses_webdriver = False
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
watch_uses_webdriver = True
from zoneinfo import available_timezones
# Only works reliably with Playwright
template_args = {
'available_processors': processors.available_processors(),
'available_timezones': sorted(available_timezones()),
'browser_steps_config': browser_step_ui_config,
'emailprefix': os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
'extra_notification_token_placeholder_info': datastore.get_unique_notification_token_placeholders_available(),
'extra_processor_config': form.extra_tab_content(),
'extra_title': f" - Edit - {watch.label}",
'form': form,
'has_default_notification_urls': True if len(datastore.data['settings']['application']['notification_urls']) else False,
'has_extra_headers_file': len(datastore.get_all_headers_in_textfile_for_watch(uuid=uuid)) > 0,
'has_special_tag_options': _watch_has_tag_options_set(watch=watch),
'watch_uses_webdriver': watch_uses_webdriver,
'jq_support': jq_support,
'playwright_enabled': os.getenv('PLAYWRIGHT_DRIVER_URL', False),
'settings_application': datastore.data['settings']['application'],
'timezone_default_config': datastore.data['settings']['application'].get('timezone'),
'using_global_webdriver_wait': not default['webdriver_delay'],
'uuid': uuid,
'watch': watch
}
included_content = None
if form.extra_form_content():
# So that the extra panels can access _helpers.html etc, we set the environment to load from templates/
# And then render the code from the module
templates_dir = str(importlib.resources.files("changedetectionio").joinpath('templates'))
env = Environment(loader=FileSystemLoader(templates_dir))
template = env.from_string(form.extra_form_content())
included_content = template.render(**template_args)
output = render_template("edit.html",
extra_tab_content=form.extra_tab_content() if form.extra_tab_content() else None,
extra_form_content=included_content,
**template_args
)
return output
@edit_blueprint.route("/edit/<string:uuid>/get-html", methods=['GET'])
@login_optionally_required
def watch_get_latest_html(uuid):
from io import BytesIO
from flask import send_file
import brotli
watch = datastore.data['watching'].get(uuid)
if watch and watch.history.keys() and os.path.isdir(watch.watch_data_dir):
latest_filename = list(watch.history.keys())[-1]
html_fname = os.path.join(watch.watch_data_dir, f"{latest_filename}.html.br")
with open(html_fname, 'rb') as f:
if html_fname.endswith('.br'):
# Read and decompress the Brotli file
decompressed_data = brotli.decompress(f.read())
else:
decompressed_data = f.read()
buffer = BytesIO(decompressed_data)
return send_file(buffer, as_attachment=True, download_name=f"{latest_filename}.html", mimetype='text/html')
# Return a 500 error
abort(500)
# Ajax callback
@edit_blueprint.route("/edit/<string:uuid>/preview-rendered", methods=['POST'])
@login_optionally_required
def watch_get_preview_rendered(uuid):
'''For when viewing the "preview" of the rendered text from inside of Edit'''
from flask import jsonify
from changedetectionio.processors.text_json_diff import prepare_filter_prevew
result = prepare_filter_prevew(watch_uuid=uuid, form_data=request.form, datastore=datastore)
return jsonify(result)
@edit_blueprint.route("/highlight_submit_ignore_url", methods=['POST'])
@login_optionally_required
def highlight_submit_ignore_url():
import re
mode = request.form.get('mode')
selection = request.form.get('selection')
uuid = request.args.get('uuid','')
if datastore.data["watching"].get(uuid):
if mode == 'exact':
for l in selection.splitlines():
datastore.data["watching"][uuid]['ignore_text'].append(l.strip())
elif mode == 'digit-regex':
for l in selection.splitlines():
# Replace any series of numbers with a regex
s = re.escape(l.strip())
s = re.sub(r'[0-9]+', r'\\d+', s)
datastore.data["watching"][uuid]['ignore_text'].append('/' + s + '/')
return f"<a href={url_for('ui.ui_views.preview_page', uuid=uuid)}>Click to preview</a>"
return edit_blueprint

View File

@@ -0,0 +1,107 @@
from flask import Blueprint, request, make_response
import random
from loguru import logger
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.auth_decorator import login_optionally_required
def construct_blueprint(datastore: ChangeDetectionStore):
notification_blueprint = Blueprint('ui_notification', __name__, template_folder="../ui/templates")
# AJAX endpoint for sending a test
@notification_blueprint.route("/notification/send-test/<string:watch_uuid>", methods=['POST'])
@notification_blueprint.route("/notification/send-test", methods=['POST'])
@notification_blueprint.route("/notification/send-test/", methods=['POST'])
@login_optionally_required
def ajax_callback_send_notification_test(watch_uuid=None):
# Watch_uuid could be unset in the case it`s used in tag editor, global settings
import apprise
from changedetectionio.apprise_asset import asset
apobj = apprise.Apprise(asset=asset)
# so that the custom endpoints are registered
from changedetectionio.apprise_plugin import apprise_custom_api_call_wrapper
is_global_settings_form = request.args.get('mode', '') == 'global-settings'
is_group_settings_form = request.args.get('mode', '') == 'group-settings'
# Use an existing random one on the global/main settings form
if not watch_uuid and (is_global_settings_form or is_group_settings_form) \
and datastore.data.get('watching'):
logger.debug(f"Send test notification - Choosing random Watch {watch_uuid}")
watch_uuid = random.choice(list(datastore.data['watching'].keys()))
if not watch_uuid:
return make_response("Error: You must have atleast one watch configured for 'test notification' to work", 400)
watch = datastore.data['watching'].get(watch_uuid)
notification_urls = None
if request.form.get('notification_urls'):
notification_urls = request.form['notification_urls'].strip().splitlines()
if not notification_urls:
logger.debug("Test notification - Trying by group/tag in the edit form if available")
# On an edit page, we should also fire off to the tags if they have notifications
if request.form.get('tags') and request.form['tags'].strip():
for k in request.form['tags'].split(','):
tag = datastore.tag_exists_by_name(k.strip())
notification_urls = tag.get('notifications_urls') if tag and tag.get('notifications_urls') else None
if not notification_urls and not is_global_settings_form and not is_group_settings_form:
# In the global settings, use only what is typed currently in the text box
logger.debug("Test notification - Trying by global system settings notifications")
if datastore.data['settings']['application'].get('notification_urls'):
notification_urls = datastore.data['settings']['application']['notification_urls']
if not notification_urls:
return 'Error: No Notification URLs set/found'
for n_url in notification_urls:
if len(n_url.strip()):
if not apobj.add(n_url):
return f'Error: {n_url} is not a valid AppRise URL.'
try:
# use the same as when it is triggered, but then override it with the form test values
n_object = {
'watch_url': request.form.get('window_url', "https://changedetection.io"),
'notification_urls': notification_urls
}
# Only use if present, if not set in n_object it should use the default system value
if 'notification_format' in request.form and request.form['notification_format'].strip():
n_object['notification_format'] = request.form.get('notification_format', '').strip()
if 'notification_title' in request.form and request.form['notification_title'].strip():
n_object['notification_title'] = request.form.get('notification_title', '').strip()
elif datastore.data['settings']['application'].get('notification_title'):
n_object['notification_title'] = datastore.data['settings']['application'].get('notification_title')
else:
n_object['notification_title'] = "Test title"
if 'notification_body' in request.form and request.form['notification_body'].strip():
n_object['notification_body'] = request.form.get('notification_body', '').strip()
elif datastore.data['settings']['application'].get('notification_body'):
n_object['notification_body'] = datastore.data['settings']['application'].get('notification_body')
else:
n_object['notification_body'] = "Test body"
n_object['as_async'] = False
n_object.update(watch.extra_notification_token_values())
from changedetectionio.notification import process_notification
sent_obj = process_notification(n_object, datastore)
except Exception as e:
e_str = str(e)
# Remove this text which is not important and floods the container
e_str = e_str.replace(
"DEBUG - <class 'apprise.decorators.base.CustomNotifyPlugin.instantiate_plugin.<locals>.CustomNotifyPluginWrapper'>",
'')
return make_response(e_str, 400)
return 'OK - Sent test notifications'
return notification_blueprint

View File

@@ -3,7 +3,7 @@
<div class="box-wrap inner"> <div class="box-wrap inner">
<form <form
class="pure-form pure-form-stacked" class="pure-form pure-form-stacked"
action="{{url_for('clear_all_history')}}" action="{{url_for('ui.clear_all_history')}}"
method="POST" method="POST"
> >
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" > <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >

View File

@@ -0,0 +1,220 @@
from flask import Blueprint, request, redirect, url_for, flash, render_template, make_response, send_from_directory, abort
from flask_login import current_user
import os
import time
from copy import deepcopy
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.auth_decorator import login_optionally_required
from changedetectionio import html_tools
def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData):
views_blueprint = Blueprint('ui_views', __name__, template_folder="../ui/templates")
@views_blueprint.route("/preview/<string:uuid>", methods=['GET'])
@login_optionally_required
def preview_page(uuid):
content = []
versions = []
timestamp = None
# More for testing, possible to return the first/only
if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop()
try:
watch = datastore.data['watching'][uuid]
except KeyError:
flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('index'))
system_uses_webdriver = datastore.data['settings']['application']['fetch_backend'] == 'html_webdriver'
extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')]
is_html_webdriver = False
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
is_html_webdriver = True
triggered_line_numbers = []
if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()):
flash("Preview unavailable - No fetch/check completed or triggers not reached", "error")
else:
# So prepare the latest preview or not
preferred_version = request.args.get('version')
versions = list(watch.history.keys())
timestamp = versions[-1]
if preferred_version and preferred_version in versions:
timestamp = preferred_version
try:
versions = list(watch.history.keys())
content = watch.get_history_snapshot(timestamp)
triggered_line_numbers = html_tools.strip_ignore_text(content=content,
wordlist=watch['trigger_text'],
mode='line numbers'
)
except Exception as e:
content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''})
output = render_template("preview.html",
content=content,
current_version=timestamp,
history_n=watch.history_n,
extra_stylesheets=extra_stylesheets,
extra_title=f" - Diff - {watch.label} @ {timestamp}",
triggered_line_numbers=triggered_line_numbers,
current_diff_url=watch['url'],
screenshot=watch.get_screenshot(),
watch=watch,
uuid=uuid,
is_html_webdriver=is_html_webdriver,
last_error=watch['last_error'],
last_error_text=watch.get_error_text(),
last_error_screenshot=watch.get_error_snapshot(),
versions=versions
)
return output
@views_blueprint.route("/diff/<string:uuid>", methods=['GET', 'POST'])
@login_optionally_required
def diff_history_page(uuid):
from changedetectionio import forms
# More for testing, possible to return the first/only
if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop()
extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')]
try:
watch = datastore.data['watching'][uuid]
except KeyError:
flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('index'))
# For submission of requesting an extract
extract_form = forms.extractDataForm(request.form)
if request.method == 'POST':
if not extract_form.validate():
flash("An error occurred, please see below.", "error")
else:
extract_regex = request.form.get('extract_regex').strip()
output = watch.extract_regex_from_all_history(extract_regex)
if output:
watch_dir = os.path.join(datastore.datastore_path, uuid)
response = make_response(send_from_directory(directory=watch_dir, path=output, as_attachment=True))
response.headers['Content-type'] = 'text/csv'
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = 0
return response
flash('Nothing matches that RegEx', 'error')
redirect(url_for('ui_views.diff_history_page', uuid=uuid)+'#extract')
history = watch.history
dates = list(history.keys())
if len(dates) < 2:
flash("Not enough saved change detection snapshots to produce a report.", "error")
return redirect(url_for('index'))
# Save the current newest history as the most recently viewed
datastore.set_last_viewed(uuid, time.time())
# Read as binary and force decode as UTF-8
# Windows may fail decode in python if we just use 'r' mode (chardet decode exception)
from_version = request.args.get('from_version')
from_version_index = -2 # second newest
if from_version and from_version in dates:
from_version_index = dates.index(from_version)
else:
from_version = dates[from_version_index]
try:
from_version_file_contents = watch.get_history_snapshot(dates[from_version_index])
except Exception as e:
from_version_file_contents = f"Unable to read to-version at index {dates[from_version_index]}.\n"
to_version = request.args.get('to_version')
to_version_index = -1
if to_version and to_version in dates:
to_version_index = dates.index(to_version)
else:
to_version = dates[to_version_index]
try:
to_version_file_contents = watch.get_history_snapshot(dates[to_version_index])
except Exception as e:
to_version_file_contents = "Unable to read to-version at index{}.\n".format(dates[to_version_index])
screenshot_url = watch.get_screenshot()
system_uses_webdriver = datastore.data['settings']['application']['fetch_backend'] == 'html_webdriver'
is_html_webdriver = False
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
is_html_webdriver = True
password_enabled_and_share_is_off = False
if datastore.data['settings']['application'].get('password') or os.getenv("SALTED_PASS", False):
password_enabled_and_share_is_off = not datastore.data['settings']['application'].get('shared_diff_access')
output = render_template("diff.html",
current_diff_url=watch['url'],
from_version=str(from_version),
to_version=str(to_version),
extra_stylesheets=extra_stylesheets,
extra_title=f" - Diff - {watch.label}",
extract_form=extract_form,
is_html_webdriver=is_html_webdriver,
last_error=watch['last_error'],
last_error_screenshot=watch.get_error_snapshot(),
last_error_text=watch.get_error_text(),
left_sticky=True,
newest=to_version_file_contents,
newest_version_timestamp=dates[-1],
password_enabled_and_share_is_off=password_enabled_and_share_is_off,
from_version_file_contents=from_version_file_contents,
to_version_file_contents=to_version_file_contents,
screenshot=screenshot_url,
uuid=uuid,
versions=dates, # All except current/last
watch_a=watch
)
return output
@views_blueprint.route("/form/add/quickwatch", methods=['POST'])
@login_optionally_required
def form_quick_watch_add():
from changedetectionio import forms
form = forms.quickWatchForm(request.form)
if not form.validate():
for widget, l in form.errors.items():
flash(','.join(l), 'error')
return redirect(url_for('index'))
url = request.form.get('url').strip()
if datastore.url_exists(url):
flash(f'Warning, URL {url} already exists', "notice")
add_paused = request.form.get('edit_and_watch_submit_button') != None
processor = request.form.get('processor', 'text_json_diff')
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras={'paused': add_paused, 'processor': processor})
if new_uuid:
if add_paused:
flash('Watch added in Paused state, saving will unpause.')
return redirect(url_for('ui.ui_edit.edit_page', uuid=new_uuid, unpause_on_save=1, tag=request.args.get('tag')))
else:
# Straight into the queue.
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': new_uuid}))
flash("Watch added.")
return redirect(url_for('index', tag=request.args.get('tag','')))
return views_blueprint

View File

@@ -0,0 +1,135 @@
from flask import Blueprint
from json_logic.builtins import BUILTINS
from .exceptions import EmptyConditionRuleRowNotUsable
from .pluggy_interface import plugin_manager # Import the pluggy plugin manager
from . import default_plugin
# List of all supported JSON Logic operators
operator_choices = [
(None, "Choose one"),
(">", "Greater Than"),
("<", "Less Than"),
(">=", "Greater Than or Equal To"),
("<=", "Less Than or Equal To"),
("==", "Equals"),
("!=", "Not Equals"),
("in", "Contains"),
("!in", "Does Not Contain"),
]
# Fields available in the rules
field_choices = [
(None, "Choose one"),
]
# The data we will feed the JSON Rules to see if it passes the test/conditions or not
EXECUTE_DATA = {}
# Define the extended operations dictionary
CUSTOM_OPERATIONS = {
**BUILTINS, # Include all standard operators
}
def filter_complete_rules(ruleset):
rules = [
rule for rule in ruleset
if all(value not in ("", False, "None", None) for value in [rule["operator"], rule["field"], rule["value"]])
]
return rules
def convert_to_jsonlogic(logic_operator: str, rule_dict: list):
"""
Convert a structured rule dict into a JSON Logic rule.
:param rule_dict: Dictionary containing conditions.
:return: JSON Logic rule as a dictionary.
"""
json_logic_conditions = []
for condition in rule_dict:
operator = condition["operator"]
field = condition["field"]
value = condition["value"]
if not operator or operator == 'None' or not value or not field:
raise EmptyConditionRuleRowNotUsable()
# Convert value to int/float if possible
try:
if isinstance(value, str) and "." in value and str != "None":
value = float(value)
else:
value = int(value)
except (ValueError, TypeError):
pass # Keep as a string if conversion fails
# Handle different JSON Logic operators properly
if operator == "in":
json_logic_conditions.append({"in": [value, {"var": field}]}) # value first
elif operator in ("!", "!!", "-"):
json_logic_conditions.append({operator: [{"var": field}]}) # Unary operators
elif operator in ("min", "max", "cat"):
json_logic_conditions.append({operator: value}) # Multi-argument operators
else:
json_logic_conditions.append({operator: [{"var": field}, value]}) # Standard binary operators
return {logic_operator: json_logic_conditions} if len(json_logic_conditions) > 1 else json_logic_conditions[0]
def execute_ruleset_against_all_plugins(current_watch_uuid: str, application_datastruct, ephemeral_data={} ):
"""
Build our data and options by calling our plugins then pass it to jsonlogic and see if the conditions pass
:param ruleset: JSON Logic rule dictionary.
:param extracted_data: Dictionary containing the facts. <-- maybe the app struct+uuid
:return: Dictionary of plugin results.
"""
from json_logic import jsonLogic
EXECUTE_DATA = {}
result = True
ruleset_settings = application_datastruct['watching'].get(current_watch_uuid)
if ruleset_settings.get("conditions"):
logic_operator = "and" if ruleset_settings.get("conditions_match_logic", "ALL") == "ALL" else "or"
complete_rules = filter_complete_rules(ruleset_settings['conditions'])
if complete_rules:
# Give all plugins a chance to update the data dict again (that we will test the conditions against)
for plugin in plugin_manager.get_plugins():
new_execute_data = plugin.add_data(current_watch_uuid=current_watch_uuid,
application_datastruct=application_datastruct,
ephemeral_data=ephemeral_data)
if new_execute_data and isinstance(new_execute_data, dict):
EXECUTE_DATA.update(new_execute_data)
# Create the ruleset
ruleset = convert_to_jsonlogic(logic_operator=logic_operator, rule_dict=complete_rules)
# Pass the custom operations dictionary to jsonLogic
if not jsonLogic(logic=ruleset, data=EXECUTE_DATA, operations=CUSTOM_OPERATIONS):
result = False
return result
# Load plugins dynamically
for plugin in plugin_manager.get_plugins():
new_ops = plugin.register_operators()
if isinstance(new_ops, dict):
CUSTOM_OPERATIONS.update(new_ops)
new_operator_choices = plugin.register_operator_choices()
if isinstance(new_operator_choices, list):
operator_choices.extend(new_operator_choices)
new_field_choices = plugin.register_field_choices()
if isinstance(new_field_choices, list):
field_choices.extend(new_field_choices)

View File

@@ -0,0 +1,80 @@
# Flask Blueprint Definition
import json
from flask import Blueprint
from changedetectionio.conditions import execute_ruleset_against_all_plugins
def construct_blueprint(datastore):
from changedetectionio.flask_app import login_optionally_required
conditions_blueprint = Blueprint('conditions', __name__, template_folder="templates")
@conditions_blueprint.route("/<string:watch_uuid>/verify-condition-single-rule", methods=['POST'])
@login_optionally_required
def verify_condition_single_rule(watch_uuid):
"""Verify a single condition rule against the current snapshot"""
from changedetectionio.processors.text_json_diff import prepare_filter_prevew
from flask import request, jsonify
from copy import deepcopy
ephemeral_data = {}
# Get the watch data
watch = datastore.data['watching'].get(watch_uuid)
if not watch:
return jsonify({'status': 'error', 'message': 'Watch not found'}), 404
# First use prepare_filter_prevew to process the form data
# This will return text_after_filter which is after all current form settings are applied
# Create ephemeral data with the text from the current snapshot
try:
# Call prepare_filter_prevew to get a processed version of the content with current form settings
# We'll ignore the returned response and just use the datastore which is modified by the function
# this should apply all filters etc so then we can run the CONDITIONS against the final output text
result = prepare_filter_prevew(datastore=datastore,
form_data=request.form,
watch_uuid=watch_uuid)
ephemeral_data['text'] = result.get('after_filter', '')
# Create a temporary watch data structure with this single rule
tmp_watch_data = deepcopy(datastore.data['watching'].get(watch_uuid))
# Override the conditions in the temporary watch
rule_json = request.args.get("rule")
rule = json.loads(rule_json) if rule_json else None
# Should be key/value of field, operator, value
tmp_watch_data['conditions'] = [rule]
tmp_watch_data['conditions_match_logic'] = "ALL" # Single rule, so use ALL
# Create a temporary application data structure for the rule check
temp_app_data = {
'watching': {
watch_uuid: tmp_watch_data
}
}
# Execute the rule against the current snapshot with form data
result = execute_ruleset_against_all_plugins(
current_watch_uuid=watch_uuid,
application_datastruct=temp_app_data,
ephemeral_data=ephemeral_data
)
return jsonify({
'status': 'success',
'result': result,
'message': 'Condition passes' if result else 'Condition does not pass'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error verifying condition: {str(e)}'
}), 500
return conditions_blueprint

View File

@@ -0,0 +1,78 @@
import re
import pluggy
from price_parser import Price
from loguru import logger
hookimpl = pluggy.HookimplMarker("changedetectionio_conditions")
@hookimpl
def register_operators():
def starts_with(_, text, prefix):
return text.lower().strip().startswith(str(prefix).strip().lower())
def ends_with(_, text, suffix):
return text.lower().strip().endswith(str(suffix).strip().lower())
def length_min(_, text, strlen):
return len(text) >= int(strlen)
def length_max(_, text, strlen):
return len(text) <= int(strlen)
# ✅ Custom function for case-insensitive regex matching
def contains_regex(_, text, pattern):
"""Returns True if `text` contains `pattern` (case-insensitive regex match)."""
return bool(re.search(pattern, str(text), re.IGNORECASE))
# ✅ Custom function for NOT matching case-insensitive regex
def not_contains_regex(_, text, pattern):
"""Returns True if `text` does NOT contain `pattern` (case-insensitive regex match)."""
return not bool(re.search(pattern, str(text), re.IGNORECASE))
return {
"!contains_regex": not_contains_regex,
"contains_regex": contains_regex,
"ends_with": ends_with,
"length_max": length_max,
"length_min": length_min,
"starts_with": starts_with,
}
@hookimpl
def register_operator_choices():
return [
("starts_with", "Text Starts With"),
("ends_with", "Text Ends With"),
("length_min", "Length minimum"),
("length_max", "Length maximum"),
("contains_regex", "Text Matches Regex"),
("!contains_regex", "Text Does NOT Match Regex"),
]
@hookimpl
def register_field_choices():
return [
("extracted_number", "Extracted number after 'Filters & Triggers'"),
# ("meta_description", "Meta Description"),
# ("meta_keywords", "Meta Keywords"),
("page_filtered_text", "Page text after 'Filters & Triggers'"),
#("page_title", "Page <title>"), # actual page title <title>
]
@hookimpl
def add_data(current_watch_uuid, application_datastruct, ephemeral_data):
res = {}
if 'text' in ephemeral_data:
res['page_filtered_text'] = ephemeral_data['text']
# Better to not wrap this in try/except so that the UI can see any errors
price = Price.fromstring(ephemeral_data.get('text'))
if price and price.amount != None:
# This is slightly misleading, it's extracting a PRICE not a Number..
res['extracted_number'] = float(price.amount)
logger.debug(f"Extracted number result: '{price}' - returning float({res['extracted_number']})")
return res

View File

@@ -0,0 +1,6 @@
class EmptyConditionRuleRowNotUsable(Exception):
def __init__(self):
super().__init__("One of the 'conditions' rulesets is incomplete, cannot run.")
def __str__(self):
return self.args[0]

View File

@@ -0,0 +1,44 @@
# Condition Rule Form (for each rule row)
from wtforms import Form, SelectField, StringField, validators
from wtforms import validators
class ConditionFormRow(Form):
# ✅ Ensure Plugins Are Loaded BEFORE Importing Choices
from changedetectionio.conditions import plugin_manager
from changedetectionio.conditions import operator_choices, field_choices
field = SelectField(
"Field",
choices=field_choices,
validators=[validators.Optional()]
)
operator = SelectField(
"Operator",
choices=operator_choices,
validators=[validators.Optional()]
)
value = StringField("Value", validators=[validators.Optional()])
def validate(self, extra_validators=None):
# First, run the default validators
if not super().validate(extra_validators):
return False
# Custom validation logic
# If any of the operator/field/value is set, then they must be all set
if any(value not in ("", False, "None", None) for value in [self.operator.data, self.field.data, self.value.data]):
if not self.operator.data or self.operator.data == 'None':
self.operator.errors.append("Operator is required.")
return False
if not self.field.data or self.field.data == 'None':
self.field.errors.append("Field is required.")
return False
if not self.value.data:
self.value.errors.append("Value is required.")
return False
return True # Only return True if all conditions pass

View File

@@ -0,0 +1,44 @@
import pluggy
from . import default_plugin # Import the default plugin
# ✅ Ensure that the namespace in HookspecMarker matches PluginManager
PLUGIN_NAMESPACE = "changedetectionio_conditions"
hookspec = pluggy.HookspecMarker(PLUGIN_NAMESPACE)
hookimpl = pluggy.HookimplMarker(PLUGIN_NAMESPACE)
class ConditionsSpec:
"""Hook specifications for extending JSON Logic conditions."""
@hookspec
def register_operators():
"""Return a dictionary of new JSON Logic operators."""
pass
@hookspec
def register_operator_choices():
"""Return a list of new operator choices."""
pass
@hookspec
def register_field_choices():
"""Return a list of new field choices."""
pass
@hookspec
def add_data(current_watch_uuid, application_datastruct, ephemeral_data):
"""Add to the datadict"""
pass
# ✅ Set up Pluggy Plugin Manager
plugin_manager = pluggy.PluginManager(PLUGIN_NAMESPACE)
# ✅ Register hookspecs (Ensures they are detected)
plugin_manager.add_hookspecs(ConditionsSpec)
# ✅ Register built-in plugins manually
plugin_manager.register(default_plugin, "default_plugin")
# ✅ Discover installed plugins from external packages (if any)
plugin_manager.load_setuptools_entrypoints(PLUGIN_NAMESPACE)

View File

@@ -0,0 +1,104 @@
# Pages with a vertical height longer than this will use the 'stitch together' method.
# - Many GPUs have a max texture size of 16384x16384px (or lower on older devices).
# - If a page is taller than ~800010000px, it risks exceeding GPU memory limits.
# - This is especially important on headless Chromium, where Playwright may fail to allocate a massive full-page buffer.
# The size at which we will switch to stitching method
SCREENSHOT_SIZE_STITCH_THRESHOLD=8000
from loguru import logger
def capture_stitched_together_full_page(page):
import io
import os
import time
from PIL import Image, ImageDraw, ImageFont
MAX_TOTAL_HEIGHT = SCREENSHOT_SIZE_STITCH_THRESHOLD*4 # Maximum total height for the final image (When in stitch mode)
MAX_CHUNK_HEIGHT = 4000 # Height per screenshot chunk
WARNING_TEXT_HEIGHT = 20 # Height of the warning text overlay
# Save the original viewport size
original_viewport = page.viewport_size
now = time.time()
try:
viewport = page.viewport_size
page_height = page.evaluate("document.documentElement.scrollHeight")
# Limit the total capture height
capture_height = min(page_height, MAX_TOTAL_HEIGHT)
images = []
total_captured_height = 0
for offset in range(0, capture_height, MAX_CHUNK_HEIGHT):
# Ensure we do not exceed the total height limit
chunk_height = min(MAX_CHUNK_HEIGHT, MAX_TOTAL_HEIGHT - total_captured_height)
# Adjust viewport size for this chunk
page.set_viewport_size({"width": viewport["width"], "height": chunk_height})
# Scroll to the correct position
page.evaluate(f"window.scrollTo(0, {offset})")
# Capture screenshot chunk
screenshot_bytes = page.screenshot(type='jpeg', quality=int(os.getenv("SCREENSHOT_QUALITY", 30)))
images.append(Image.open(io.BytesIO(screenshot_bytes)))
total_captured_height += chunk_height
# Stop if we reached the maximum total height
if total_captured_height >= MAX_TOTAL_HEIGHT:
break
# Create the final stitched image
stitched_image = Image.new('RGB', (viewport["width"], total_captured_height))
y_offset = 0
# Stitch the screenshot chunks together
for img in images:
stitched_image.paste(img, (0, y_offset))
y_offset += img.height
logger.debug(f"Screenshot stitched together in {time.time()-now:.2f}s")
# Overlay warning text if the screenshot was trimmed
if page_height > MAX_TOTAL_HEIGHT:
draw = ImageDraw.Draw(stitched_image)
warning_text = f"WARNING: Screenshot was {page_height}px but trimmed to {MAX_TOTAL_HEIGHT}px because it was too long"
# Load font (default system font if Arial is unavailable)
try:
font = ImageFont.truetype("arial.ttf", WARNING_TEXT_HEIGHT) # Arial (Windows/Mac)
except IOError:
font = ImageFont.load_default() # Default font if Arial not found
# Get text bounding box (correct method for newer Pillow versions)
text_bbox = draw.textbbox((0, 0), warning_text, font=font)
text_width = text_bbox[2] - text_bbox[0] # Calculate text width
text_height = text_bbox[3] - text_bbox[1] # Calculate text height
# Define background rectangle (top of the image)
draw.rectangle([(0, 0), (viewport["width"], WARNING_TEXT_HEIGHT)], fill="white")
# Center text horizontally within the warning area
text_x = (viewport["width"] - text_width) // 2
text_y = (WARNING_TEXT_HEIGHT - text_height) // 2
# Draw the warning text in red
draw.text((text_x, text_y), warning_text, fill="red", font=font)
# Save or return the final image
output = io.BytesIO()
stitched_image.save(output, format="JPEG", quality=int(os.getenv("SCREENSHOT_QUALITY", 30)))
screenshot = output.getvalue()
finally:
# Restore the original viewport size
page.set_viewport_size(original_viewport)
return screenshot

View File

@@ -4,6 +4,7 @@ from urllib.parse import urlparse
from loguru import logger from loguru import logger
from changedetectionio.content_fetchers.helpers import capture_stitched_together_full_page, SCREENSHOT_SIZE_STITCH_THRESHOLD
from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, ScreenshotUnavailable from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, ScreenshotUnavailable
@@ -89,6 +90,7 @@ class fetcher(Fetcher):
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
import playwright._impl._errors import playwright._impl._errors
from changedetectionio.content_fetchers import visualselector_xpath_selectors from changedetectionio.content_fetchers import visualselector_xpath_selectors
import time
self.delete_browser_steps_screenshots() self.delete_browser_steps_screenshots()
response = None response = None
@@ -179,6 +181,7 @@ class fetcher(Fetcher):
self.page.wait_for_timeout(extra_wait * 1000) self.page.wait_for_timeout(extra_wait * 1000)
now = time.time()
# So we can find an element on the page where its selector was entered manually (maybe not xPath etc) # So we can find an element on the page where its selector was entered manually (maybe not xPath etc)
if current_include_filters is not None: if current_include_filters is not None:
self.page.evaluate("var include_filters={}".format(json.dumps(current_include_filters))) self.page.evaluate("var include_filters={}".format(json.dumps(current_include_filters)))
@@ -190,6 +193,8 @@ class fetcher(Fetcher):
self.instock_data = self.page.evaluate("async () => {" + self.instock_data_js + "}") self.instock_data = self.page.evaluate("async () => {" + self.instock_data_js + "}")
self.content = self.page.content() self.content = self.page.content()
logger.debug(f"Time to scrape xpath element data in browser {time.time() - now:.2f}s")
# Bug 3 in Playwright screenshot handling # Bug 3 in Playwright screenshot handling
# Some bug where it gives the wrong screenshot size, but making a request with the clip set first seems to solve it # Some bug where it gives the wrong screenshot size, but making a request with the clip set first seems to solve it
# JPEG is better here because the screenshots can be very very large # JPEG is better here because the screenshots can be very very large
@@ -199,10 +204,15 @@ class fetcher(Fetcher):
# acceptable screenshot quality here # acceptable screenshot quality here
try: try:
# The actual screenshot - this always base64 and needs decoding! horrible! huge CPU usage # The actual screenshot - this always base64 and needs decoding! horrible! huge CPU usage
self.screenshot = self.page.screenshot(type='jpeg', full_height = self.page.evaluate("document.documentElement.scrollHeight")
full_page=True,
quality=int(os.getenv("SCREENSHOT_QUALITY", 72)), if full_height >= SCREENSHOT_SIZE_STITCH_THRESHOLD:
) logger.warning(
f"Page full Height: {full_height}px longer than {SCREENSHOT_SIZE_STITCH_THRESHOLD}px, using 'stitched screenshot method'.")
self.screenshot = capture_stitched_together_full_page(self.page)
else:
self.screenshot = self.page.screenshot(type='jpeg', full_page=True, quality=int(os.getenv("SCREENSHOT_QUALITY", 30)))
except Exception as e: except Exception as e:
# It's likely the screenshot was too long/big and something crashed # It's likely the screenshot was too long/big and something crashed
raise ScreenshotUnavailable(url=url, status_code=self.status_code) raise ScreenshotUnavailable(url=url, status_code=self.status_code)

View File

@@ -29,8 +29,11 @@ function isItemInStock() {
'currently unavailable', 'currently unavailable',
'dieser artikel ist bald wieder verfügbar', 'dieser artikel ist bald wieder verfügbar',
'dostępne wkrótce', 'dostępne wkrótce',
'en rupture',
'en rupture de stock', 'en rupture de stock',
'épuisé',
'esgotado', 'esgotado',
'indisponible',
'indisponível', 'indisponível',
'isn\'t in stock right now', 'isn\'t in stock right now',
'isnt in stock right now', 'isnt in stock right now',
@@ -52,6 +55,8 @@ function isItemInStock() {
'niet leverbaar', 'niet leverbaar',
'niet op voorraad', 'niet op voorraad',
'no disponible', 'no disponible',
'non disponibile',
'non disponible',
'no longer in stock', 'no longer in stock',
'no tickets available', 'no tickets available',
'not available', 'not available',
@@ -64,8 +69,10 @@ function isItemInStock() {
'não estamos a aceitar encomendas', 'não estamos a aceitar encomendas',
'out of stock', 'out of stock',
'out-of-stock', 'out-of-stock',
'plus disponible',
'prodotto esaurito', 'prodotto esaurito',
'produkt niedostępny', 'produkt niedostępny',
'rupture',
'sold out', 'sold out',
'sold-out', 'sold-out',
'stokta yok', 'stokta yok',

View File

@@ -41,7 +41,7 @@ const findUpTag = (el) => {
// Strategy 1: If it's an input, with name, and there's only one, prefer that // Strategy 1: If it's an input, with name, and there's only one, prefer that
if (el.name !== undefined && el.name.length) { if (el.name !== undefined && el.name.length) {
var proposed = el.tagName + "[name=" + el.name + "]"; var proposed = el.tagName + "[name=\"" + CSS.escape(el.name) + "\"]";
var proposed_element = window.document.querySelectorAll(proposed); var proposed_element = window.document.querySelectorAll(proposed);
if (proposed_element.length) { if (proposed_element.length) {
if (proposed_element.length === 1) { if (proposed_element.length === 1) {
@@ -102,13 +102,15 @@ function collectVisibleElements(parent, visibleElements) {
const children = parent.children; const children = parent.children;
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i]; const child = children[i];
const computedStyle = window.getComputedStyle(child);
if ( if (
child.nodeType === Node.ELEMENT_NODE && child.nodeType === Node.ELEMENT_NODE &&
window.getComputedStyle(child).display !== 'none' && computedStyle.display !== 'none' &&
window.getComputedStyle(child).visibility !== 'hidden' && computedStyle.visibility !== 'hidden' &&
child.offsetWidth >= 0 && child.offsetWidth >= 0 &&
child.offsetHeight >= 0 && child.offsetHeight >= 0 &&
window.getComputedStyle(child).contentVisibility !== 'hidden' computedStyle.contentVisibility !== 'hidden'
) { ) {
// If the child is an element and is visible, recursively collect visible elements // If the child is an element and is visible, recursively collect visible elements
collectVisibleElements(child, visibleElements); collectVisibleElements(child, visibleElements);
@@ -173,6 +175,7 @@ visibleElementsArray.forEach(function (element) {
// Try to identify any possible currency amounts "Sale: 4000" or "Sale now 3000 Kc", can help with the training. // Try to identify any possible currency amounts "Sale: 4000" or "Sale now 3000 Kc", can help with the training.
const hasDigitCurrency = (/\d/.test(text.slice(0, 6)) || /\d/.test(text.slice(-6)) ) && /([€£$¥₩₹]|USD|AUD|EUR|Kč|kr|SEK|,)/.test(text) ; const hasDigitCurrency = (/\d/.test(text.slice(0, 6)) || /\d/.test(text.slice(-6)) ) && /([€£$¥₩₹]|USD|AUD|EUR|Kč|kr|SEK|,)/.test(text) ;
const computedStyle = window.getComputedStyle(element);
size_pos.push({ size_pos.push({
xpath: xpath_result, xpath: xpath_result,
@@ -184,10 +187,10 @@ visibleElementsArray.forEach(function (element) {
tagName: (element.tagName) ? element.tagName.toLowerCase() : '', tagName: (element.tagName) ? element.tagName.toLowerCase() : '',
// tagtype used by Browser Steps // tagtype used by Browser Steps
tagtype: (element.tagName.toLowerCase() === 'input' && element.type) ? element.type.toLowerCase() : '', tagtype: (element.tagName.toLowerCase() === 'input' && element.type) ? element.type.toLowerCase() : '',
isClickable: window.getComputedStyle(element).cursor === "pointer", isClickable: computedStyle.cursor === "pointer",
// Used by the keras trainer // Used by the keras trainer
fontSize: window.getComputedStyle(element).getPropertyValue('font-size'), fontSize: computedStyle.getPropertyValue('font-size'),
fontWeight: window.getComputedStyle(element).getPropertyValue('font-weight'), fontWeight: computedStyle.getPropertyValue('font-weight'),
hasDigitCurrency: hasDigitCurrency, hasDigitCurrency: hasDigitCurrency,
label: label, label: label,
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import re
from loguru import logger from loguru import logger
from wtforms.widgets.core import TimeInput from wtforms.widgets.core import TimeInput
from changedetectionio.conditions.form import ConditionFormRow
from changedetectionio.strtobool import strtobool from changedetectionio.strtobool import strtobool
from wtforms import ( from wtforms import (
@@ -171,7 +172,7 @@ class validateTimeZoneName(object):
class ScheduleLimitDaySubForm(Form): class ScheduleLimitDaySubForm(Form):
enabled = BooleanField("not set", default=True) enabled = BooleanField("not set", default=True)
start_time = TimeStringField("Start At", default="00:00", render_kw={"placeholder": "HH:MM"}, validators=[validators.Optional()]) start_time = TimeStringField("Start At", default="00:00", validators=[validators.Optional()])
duration = FormField(TimeDurationForm, label="Run duration") duration = FormField(TimeDurationForm, label="Run duration")
class ScheduleLimitForm(Form): class ScheduleLimitForm(Form):
@@ -305,8 +306,10 @@ class ValidateAppRiseServers(object):
def __call__(self, form, field): def __call__(self, form, field):
import apprise import apprise
apobj = apprise.Apprise() apobj = apprise.Apprise()
# so that the custom endpoints are registered # so that the custom endpoints are registered
from changedetectionio.apprise_plugin import apprise_custom_api_call_wrapper from .apprise_asset import asset
for server_url in field.data: for server_url in field.data:
url = server_url.strip() url = server_url.strip()
if url.startswith("#"): if url.startswith("#"):
@@ -509,6 +512,7 @@ class quickWatchForm(Form):
edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"}) edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"})
# Common to a single watch and the global settings # Common to a single watch and the global settings
class commonSettingsForm(Form): class commonSettingsForm(Form):
from . import processors from . import processors
@@ -596,6 +600,10 @@ class processor_text_json_diff_form(commonSettingsForm):
notification_muted = BooleanField('Notifications Muted / Off', default=False) notification_muted = BooleanField('Notifications Muted / Off', default=False)
notification_screenshot = BooleanField('Attach screenshot to notification (where possible)', default=False) notification_screenshot = BooleanField('Attach screenshot to notification (where possible)', default=False)
conditions_match_logic = RadioField(u'Match', choices=[('ALL', 'Match all of the following'),('ANY', 'Match any of the following')], default='ALL')
conditions = FieldList(FormField(ConditionFormRow), min_entries=1) # Add rule logic here
def extra_tab_content(self): def extra_tab_content(self):
return None return None

View File

@@ -1,5 +1,6 @@
from typing import List from loguru import logger
from lxml import etree from lxml import etree
from typing import List
import json import json
import re import re
@@ -298,8 +299,10 @@ def extract_json_as_string(content, json_filter, ensure_is_ldjson_info_type=None
# https://github.com/dgtlmoon/changedetection.io/pull/2041#issuecomment-1848397161w # https://github.com/dgtlmoon/changedetection.io/pull/2041#issuecomment-1848397161w
# Try to parse/filter out the JSON, if we get some parser error, then maybe it's embedded within HTML tags # Try to parse/filter out the JSON, if we get some parser error, then maybe it's embedded within HTML tags
try: try:
stripped_text_from_html = _parse_json(json.loads(content), json_filter) # .lstrip("\ufeff") strings ByteOrderMark from UTF8 and still lets the UTF work
except json.JSONDecodeError: stripped_text_from_html = _parse_json(json.loads(content.lstrip("\ufeff") ), json_filter)
except json.JSONDecodeError as e:
logger.warning(str(e))
# Foreach <script json></script> blob.. just return the first that matches json_filter # Foreach <script json></script> blob.. just return the first that matches json_filter
# As a last resort, try to parse the whole <body> # As a last resort, try to parse the whole <body>

View File

@@ -69,7 +69,7 @@ def parse_headers_from_text_file(filepath):
for l in f.readlines(): for l in f.readlines():
l = l.strip() l = l.strip()
if not l.startswith('#') and ':' in l: if not l.startswith('#') and ':' in l:
(k, v) = l.split(':') (k, v) = l.split(':', 1) # Split only on the first colon
headers[k.strip()] = v.strip() headers[k.strip()] = v.strip()
return headers return headers

View File

@@ -83,7 +83,7 @@ class model(watch_base):
flash, Markup, url_for flash, Markup, url_for
) )
message = Markup('<a href="{}#general">The URL {} is invalid and cannot be used, click to edit</a>'.format( message = Markup('<a href="{}#general">The URL {} is invalid and cannot be used, click to edit</a>'.format(
url_for('edit_page', uuid=self.get('uuid')), self.get('url', ''))) url_for('ui.ui_edit.edit_page', uuid=self.get('uuid')), self.get('url', '')))
flash(message, 'error') flash(message, 'error')
return '' return ''
@@ -296,11 +296,11 @@ class model(watch_base):
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
return f.read() return f.read()
# Save some text file to the appropriate path and bump the history # Save some text file to the appropriate path and bump the history
# result_obj from fetch_site_status.run() # result_obj from fetch_site_status.run()
def save_history_text(self, contents, timestamp, snapshot_id): def save_history_text(self, contents, timestamp, snapshot_id):
import brotli import brotli
import tempfile
logger.trace(f"{self.get('uuid')} - Updating history.txt with timestamp {timestamp}") logger.trace(f"{self.get('uuid')} - Updating history.txt with timestamp {timestamp}")
self.ensure_data_dir_exists() self.ensure_data_dir_exists()
@@ -308,26 +308,37 @@ class model(watch_base):
threshold = int(os.getenv('SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD', 1024)) threshold = int(os.getenv('SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD', 1024))
skip_brotli = strtobool(os.getenv('DISABLE_BROTLI_TEXT_SNAPSHOT', 'False')) skip_brotli = strtobool(os.getenv('DISABLE_BROTLI_TEXT_SNAPSHOT', 'False'))
# Decide on snapshot filename and destination path
if not skip_brotli and len(contents) > threshold: if not skip_brotli and len(contents) > threshold:
snapshot_fname = f"{snapshot_id}.txt.br" snapshot_fname = f"{snapshot_id}.txt.br"
dest = os.path.join(self.watch_data_dir, snapshot_fname) encoded_data = brotli.compress(contents.encode('utf-8'), mode=brotli.MODE_TEXT)
if not os.path.exists(dest):
with open(dest, 'wb') as f:
f.write(brotli.compress(contents.encode('utf-8'), mode=brotli.MODE_TEXT))
else: else:
snapshot_fname = f"{snapshot_id}.txt" snapshot_fname = f"{snapshot_id}.txt"
dest = os.path.join(self.watch_data_dir, snapshot_fname) encoded_data = contents.encode('utf-8')
if not os.path.exists(dest):
with open(dest, 'wb') as f:
f.write(contents.encode('utf-8'))
# Append to index dest = os.path.join(self.watch_data_dir, snapshot_fname)
# @todo check last char was \n
# Write snapshot file atomically if it doesn't exist
if not os.path.exists(dest):
with tempfile.NamedTemporaryFile('wb', delete=False, dir=self.watch_data_dir) as tmp:
tmp.write(encoded_data)
tmp.flush()
os.fsync(tmp.fileno())
tmp_path = tmp.name
os.rename(tmp_path, dest)
# Append to history.txt atomically
index_fname = os.path.join(self.watch_data_dir, "history.txt") index_fname = os.path.join(self.watch_data_dir, "history.txt")
with open(index_fname, 'a') as f: index_line = f"{timestamp},{snapshot_fname}\n"
f.write("{},{}\n".format(timestamp, snapshot_fname))
f.close()
# Lets try force flush here since it's usually a very small file
# If this still fails in the future then try reading all to memory first, re-writing etc
with open(index_fname, 'a', encoding='utf-8') as f:
f.write(index_line)
f.flush()
os.fsync(f.fileno())
# Update internal state
self.__newest_history_key = timestamp self.__newest_history_key = timestamp
self.__history_n += 1 self.__history_n += 1
@@ -352,7 +363,7 @@ class model(watch_base):
# Iterate over all history texts and see if something new exists # Iterate over all history texts and see if something new exists
# Always applying .strip() to start/end but optionally replace any other whitespace # Always applying .strip() to start/end but optionally replace any other whitespace
def lines_contain_something_unique_compared_to_history(self, lines: list, ignore_whitespace=False): def lines_contain_something_unique_compared_to_history(self, lines: list, ignore_whitespace=False):
local_lines = [] local_lines = set([])
if lines: if lines:
if ignore_whitespace: if ignore_whitespace:
if isinstance(lines[0], str): # Can be either str or bytes depending on what was on the disk if isinstance(lines[0], str): # Can be either str or bytes depending on what was on the disk
@@ -527,7 +538,7 @@ class model(watch_base):
def save_error_text(self, contents): def save_error_text(self, contents):
self.ensure_data_dir_exists() self.ensure_data_dir_exists()
target_path = os.path.join(self.watch_data_dir, "last-error.txt") target_path = os.path.join(self.watch_data_dir, "last-error.txt")
with open(target_path, 'w') as f: with open(target_path, 'w', encoding='utf-8') as f:
f.write(contents) f.write(contents)
def save_xpath_data(self, data, as_error=False): def save_xpath_data(self, data, as_error=False):

View File

@@ -28,13 +28,13 @@ def _task(watch, update_handler):
return text_after_filter return text_after_filter
def prepare_filter_prevew(datastore, watch_uuid): def prepare_filter_prevew(datastore, watch_uuid, form_data):
'''Used by @app.route("/edit/<string:uuid>/preview-rendered", methods=['POST'])''' '''Used by @app.route("/edit/<string:uuid>/preview-rendered", methods=['POST'])'''
from changedetectionio import forms, html_tools from changedetectionio import forms, html_tools
from changedetectionio.model.Watch import model as watch_model from changedetectionio.model.Watch import model as watch_model
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from copy import deepcopy from copy import deepcopy
from flask import request, jsonify from flask import request
import brotli import brotli
import importlib import importlib
import os import os
@@ -50,12 +50,12 @@ def prepare_filter_prevew(datastore, watch_uuid):
if tmp_watch and tmp_watch.history and os.path.isdir(tmp_watch.watch_data_dir): if tmp_watch and tmp_watch.history and os.path.isdir(tmp_watch.watch_data_dir):
# Splice in the temporary stuff from the form # Splice in the temporary stuff from the form
form = forms.processor_text_json_diff_form(formdata=request.form if request.method == 'POST' else None, form = forms.processor_text_json_diff_form(formdata=form_data if request.method == 'POST' else None,
data=request.form data=form_data
) )
# Only update vars that came in via the AJAX post # Only update vars that came in via the AJAX post
p = {k: v for k, v in form.data.items() if k in request.form.keys()} p = {k: v for k, v in form.data.items() if k in form_data.keys()}
tmp_watch.update(p) tmp_watch.update(p)
blank_watch_no_filters = watch_model() blank_watch_no_filters = watch_model()
blank_watch_no_filters['url'] = tmp_watch.get('url') blank_watch_no_filters['url'] = tmp_watch.get('url')
@@ -103,13 +103,12 @@ def prepare_filter_prevew(datastore, watch_uuid):
logger.trace(f"Parsed in {time.time() - now:.3f}s") logger.trace(f"Parsed in {time.time() - now:.3f}s")
return jsonify( return ({
{
'after_filter': text_after_filter, 'after_filter': text_after_filter,
'before_filter': text_before_filter.decode('utf-8') if isinstance(text_before_filter, bytes) else text_before_filter, 'before_filter': text_before_filter.decode('utf-8') if isinstance(text_before_filter, bytes) else text_before_filter,
'duration': time.time() - now, 'duration': time.time() - now,
'trigger_line_numbers': trigger_line_numbers, 'trigger_line_numbers': trigger_line_numbers,
'ignore_line_numbers': ignore_line_numbers, 'ignore_line_numbers': ignore_line_numbers,
} })
)

View File

@@ -6,6 +6,7 @@ import os
import re import re
import urllib3 import urllib3
from changedetectionio.conditions import execute_ruleset_against_all_plugins
from changedetectionio.processors import difference_detection_processor from changedetectionio.processors import difference_detection_processor
from changedetectionio.html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text, TRANSLATE_WHITESPACE_TABLE from changedetectionio.html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text, TRANSLATE_WHITESPACE_TABLE
from changedetectionio import html_tools, content_fetchers from changedetectionio import html_tools, content_fetchers
@@ -331,6 +332,16 @@ class perform_site_check(difference_detection_processor):
if result: if result:
blocked = True blocked = True
# And check if 'conditions' will let this pass through
if watch.get('conditions') and watch.get('conditions_match_logic'):
if not execute_ruleset_against_all_plugins(current_watch_uuid=watch.get('uuid'),
application_datastruct=self.datastore.data,
ephemeral_data={
'text': stripped_text_from_html
}
):
# Conditions say "Condition not met" so we block it.
blocked = True
# Looks like something changed, but did it match all the rules? # Looks like something changed, but did it match all the rules?
if blocked: if blocked:

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg <svg
version="1.1" version="1.1"
id="Layer_1" id="copy"
x="0px" x="0px"
y="0px" y="0px"
viewBox="0 0 115.77 122.88" viewBox="0 0 115.77 122.88"

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -6,7 +6,7 @@
height="7.5005589" height="7.5005589"
width="11.248507" width="11.248507"
version="1.1" version="1.1"
id="Layer_1" id="email"
viewBox="0 0 7.1975545 4.7993639" viewBox="0 0 7.1975545 4.7993639"
xml:space="preserve" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg <svg
version="1.1" version="1.1"
id="Layer_1" id="schedule"
x="0px" x="0px"
y="0px" y="0px"
viewBox="0 0 661.20001 665.40002" viewBox="0 0 661.20001 665.40002"

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -221,7 +221,7 @@ $(document).ready(function () {
// If you switch to "Click X,y" after an element here is setup, it will give the last co-ords anyway // If you switch to "Click X,y" after an element here is setup, it will give the last co-ords anyway
//if (x['isClickable'] || x['tagName'].startsWith('h') || x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit' || x['tagtype'] === 'checkbox' || x['tagtype'] === 'radio' || x['tagtype'] === 'li') { //if (x['isClickable'] || x['tagName'].startsWith('h') || x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit' || x['tagtype'] === 'checkbox' || x['tagtype'] === 'radio' || x['tagtype'] === 'li') {
$('select', first_available).val('Click element').change(); $('select', first_available).val('Click element').change();
$('input[type=text]', first_available).first().val(x['xpath']); $('input[type=text]', first_available).first().val(x['xpath']).focus();
found_something = true; found_something = true;
//} //}
} }
@@ -305,7 +305,7 @@ $(document).ready(function () {
if ($(this).val() === 'Click X,Y' && last_click_xy['x'] > 0 && $(elem_value).val().length === 0) { if ($(this).val() === 'Click X,Y' && last_click_xy['x'] > 0 && $(elem_value).val().length === 0) {
// @todo handle scale // @todo handle scale
$(elem_value).val(last_click_xy['x'] + ',' + last_click_xy['y']); $(elem_value).val(last_click_xy['x'] + ',' + last_click_xy['y']).focus();
} }
}).change(); }).change();

View File

@@ -0,0 +1,150 @@
$(document).ready(function () {
// Function to set up button event handlers
function setupButtonHandlers() {
// Unbind existing handlers first to prevent duplicates
$(".addRuleRow, .removeRuleRow, .verifyRuleRow").off("click");
// Add row button handler
$(".addRuleRow").on("click", function(e) {
e.preventDefault();
let currentRow = $(this).closest("tr");
// Clone without events
let newRow = currentRow.clone(false);
// Reset input values in the cloned row
newRow.find("input").val("");
newRow.find("select").prop("selectedIndex", 0);
// Insert the new row after the current one
currentRow.after(newRow);
// Reindex all rows
reindexRules();
});
// Remove row button handler
$(".removeRuleRow").on("click", function(e) {
e.preventDefault();
// Only remove if there's more than one row
if ($("#rulesTable tbody tr").length > 1) {
$(this).closest("tr").remove();
reindexRules();
}
});
// Verify rule button handler
$(".verifyRuleRow").on("click", function(e) {
e.preventDefault();
let row = $(this).closest("tr");
let field = row.find("select[name$='field']").val();
let operator = row.find("select[name$='operator']").val();
let value = row.find("input[name$='value']").val();
// Validate that all fields are filled
if (!field || field === "None" || !operator || operator === "None" || !value) {
alert("Please fill in all fields (Field, Operator, and Value) before verifying.");
return;
}
// Create a rule object
const rule = {
field: field,
operator: operator,
value: value
};
// Show a spinner or some indication that verification is in progress
const $button = $(this);
const originalHTML = $button.html();
$button.html("⌛").prop("disabled", true);
// Collect form data - similar to request_textpreview_update() in watch-settings.js
let formData = new FormData();
$('#edit-text-filter textarea, #edit-text-filter input').each(function() {
const $element = $(this);
const name = $element.attr('name');
if (name) {
if ($element.is(':checkbox')) {
formData.append(name, $element.is(':checked') ? $element.val() : false);
} else {
formData.append(name, $element.val());
}
}
});
// Also collect select values
$('#edit-text-filter select').each(function() {
const $element = $(this);
const name = $element.attr('name');
if (name) {
formData.append(name, $element.val());
}
});
// Send the request to verify the rule
$.ajax({
url: verify_condition_rule_url+"?"+ new URLSearchParams({ rule: JSON.stringify(rule) }).toString(),
type: "POST",
data: formData,
processData: false, // Prevent jQuery from converting FormData to a string
contentType: false, // Let the browser set the correct content type
success: function (response) {
if (response.status === "success") {
if (response.result) {
alert("✅ Condition PASSES verification against current snapshot!");
} else {
alert("❌ Condition FAILS verification against current snapshot.");
}
} else {
alert("Error: " + response.message);
}
$button.html(originalHTML).prop("disabled", false);
},
error: function (xhr) {
let errorMsg = "Error verifying condition.";
if (xhr.responseJSON && xhr.responseJSON.message) {
errorMsg = xhr.responseJSON.message;
}
alert(errorMsg);
$button.html(originalHTML).prop("disabled", false);
}
});
});
}
// Function to reindex form elements and re-setup event handlers
function reindexRules() {
// Unbind all button handlers first
$(".addRuleRow, .removeRuleRow, .verifyRuleRow").off("click");
// Reindex all form elements
$("#rulesTable tbody tr").each(function(index) {
$(this).find("select, input").each(function() {
let oldName = $(this).attr("name");
let oldId = $(this).attr("id");
if (oldName) {
let newName = oldName.replace(/\d+/, index);
$(this).attr("name", newName);
}
if (oldId) {
let newId = oldId.replace(/\d+/, index);
$(this).attr("id", newId);
}
});
});
// Reattach event handlers after reindexing
setupButtonHandlers();
}
// Initial setup of button handlers
setupButtonHandlers();
});

View File

@@ -26,7 +26,6 @@ function set_active_tab() {
if (tab.length) { if (tab.length) {
tab[0].parentElement.className = "active"; tab[0].parentElement.className = "active";
} }
} }
function focus_error_tab() { function focus_error_tab() {

View File

@@ -40,19 +40,22 @@
} }
} }
@media only screen and (min-width: 760px) {
#browser-steps .flex-wrapper { #browser-steps .flex-wrapper {
display: flex; display: flex;
flex-flow: row; flex-flow: row;
height: 70vh; height: 70vh;
font-size: 80%; font-size: 80%;
#browser-steps-ui {
flex-grow: 1; /* Allow it to grow and fill the available space */
flex-shrink: 1; /* Allow it to shrink if needed */
flex-basis: 0; /* Start with 0 base width so it stretches as much as possible */
background-color: #eee;
border-radius: 5px;
#browser-steps-ui {
flex-grow: 1; /* Allow it to grow and fill the available space */
flex-shrink: 1; /* Allow it to shrink if needed */
flex-basis: 0; /* Start with 0 base width so it stretches as much as possible */
background-color: #eee;
border-radius: 5px;
}
} }
#browser-steps-fieldlist { #browser-steps-fieldlist {
@@ -63,15 +66,21 @@
padding-left: 1rem; padding-left: 1rem;
overflow-y: scroll; overflow-y: scroll;
} }
/* this is duplicate :( */
#browsersteps-selector-wrapper {
height: 100% !important;
}
} }
/* this is duplicate :( */ /* this is duplicate :( */
#browsersteps-selector-wrapper { #browsersteps-selector-wrapper {
height: 100%;
width: 100%; width: 100%;
overflow-y: scroll; overflow-y: scroll;
position: relative; position: relative;
//width: 100%; height: 80vh;
> img { > img {
position: absolute; position: absolute;
max-width: 100%; max-width: 100%;
@@ -91,7 +100,6 @@
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
margin-left: -40px;
z-index: 100; z-index: 100;
max-width: 350px; max-width: 350px;
text-align: center; text-align: center;

View File

@@ -0,0 +1,9 @@
ul#conditions_match_logic {
list-style: none;
input, label, li {
display: inline-block;
}
li {
padding-right: 1em;
}
}

View File

@@ -13,6 +13,7 @@
@import "parts/_menu"; @import "parts/_menu";
@import "parts/_love"; @import "parts/_love";
@import "parts/preview_text_filter"; @import "parts/preview_text_filter";
@import "parts/_edit";
body { body {
color: var(--color-text); color: var(--color-text);

View File

@@ -46,21 +46,22 @@
#browser_steps li > label { #browser_steps li > label {
display: none; } display: none; }
#browser-steps .flex-wrapper { @media only screen and (min-width: 760px) {
display: flex; #browser-steps .flex-wrapper {
flex-flow: row; display: flex;
height: 70vh; flex-flow: row;
font-size: 80%; } height: 70vh;
#browser-steps .flex-wrapper #browser-steps-ui { font-size: 80%; }
flex-grow: 1; #browser-steps .flex-wrapper #browser-steps-ui {
/* Allow it to grow and fill the available space */ flex-grow: 1;
flex-shrink: 1; /* Allow it to grow and fill the available space */
/* Allow it to shrink if needed */ flex-shrink: 1;
flex-basis: 0; /* Allow it to shrink if needed */
/* Start with 0 base width so it stretches as much as possible */ flex-basis: 0;
background-color: #eee; /* Start with 0 base width so it stretches as much as possible */
border-radius: 5px; } background-color: #eee;
#browser-steps .flex-wrapper #browser-steps-fieldlist { border-radius: 5px; }
#browser-steps-fieldlist {
flex-grow: 0; flex-grow: 0;
/* Don't allow it to grow */ /* Don't allow it to grow */
flex-shrink: 0; flex-shrink: 0;
@@ -71,13 +72,16 @@
/* Set a max width to prevent overflow */ /* Set a max width to prevent overflow */
padding-left: 1rem; padding-left: 1rem;
overflow-y: scroll; } overflow-y: scroll; }
/* this is duplicate :( */
#browsersteps-selector-wrapper {
height: 100% !important; } }
/* this is duplicate :( */ /* this is duplicate :( */
#browsersteps-selector-wrapper { #browsersteps-selector-wrapper {
height: 100%;
width: 100%; width: 100%;
overflow-y: scroll; overflow-y: scroll;
position: relative; position: relative;
height: 80vh;
/* nice tall skinny one */ } /* nice tall skinny one */ }
#browsersteps-selector-wrapper > img { #browsersteps-selector-wrapper > img {
position: absolute; position: absolute;
@@ -92,7 +96,6 @@
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
margin-left: -40px;
z-index: 100; z-index: 100;
max-width: 350px; max-width: 350px;
text-align: center; } text-align: center; }
@@ -520,6 +523,13 @@ body.preview-text-enabled {
z-index: 3; z-index: 3;
box-shadow: 1px 1px 4px var(--color-shadow-jump); } box-shadow: 1px 1px 4px var(--color-shadow-jump); }
ul#conditions_match_logic {
list-style: none; }
ul#conditions_match_logic input, ul#conditions_match_logic label, ul#conditions_match_logic li {
display: inline-block; }
ul#conditions_match_logic li {
padding-right: 1em; }
body { body {
color: var(--color-text); color: var(--color-text);
background: var(--color-background-page); background: var(--color-background-page);

View File

@@ -12,13 +12,13 @@
}} }}
<div class="pure-form-message-inline"> <div class="pure-form-message-inline">
<p> <p>
<strong>Tip:</strong> Use <a target=_new href="https://github.com/caronc/apprise">AppRise Notification URLs</a> for notification to just about any service! <i><a target=_new href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes">Please read the notification services wiki here for important configuration notes</a></i>.<br> <strong>Tip:</strong> Use <a target="newwindow" href="https://github.com/caronc/apprise">AppRise Notification URLs</a> for notification to just about any service! <i><a target="newwindow" href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes">Please read the notification services wiki here for important configuration notes</a></i>.<br>
</p> </p>
<div data-target="#advanced-help-notifications" class="toggle-show pure-button button-tag button-xsmall">Show advanced help and tips</div> <div data-target="#advanced-help-notifications" class="toggle-show pure-button button-tag button-xsmall">Show advanced help and tips</div>
<ul style="display: none" id="advanced-help-notifications"> <ul style="display: none" id="advanced-help-notifications">
<li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_discord">discord://</a></code> (or <code>https://discord.com/api/webhooks...</code>)) only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li> <li><code><a target="newwindow" href="https://github.com/caronc/apprise/wiki/Notify_discord">discord://</a></code> (or <code>https://discord.com/api/webhooks...</code>)) only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li>
<li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> bots can't send messages to other bots, so you should specify chat ID of non-bot user.</li> <li><code><a target="newwindow" href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> bots can't send messages to other bots, so you should specify chat ID of non-bot user.</li>
<li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li> <li><code><a target="newwindow" href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li>
<li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>) <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes#postposts">more help here</a></li> <li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>) <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes#postposts">more help here</a></li>
<li>Accepts the <code>{{ '{{token}}' }}</code> placeholders listed below</li> <li>Accepts the <code>{{ '{{token}}' }}</code> placeholders listed below</li>
</ul> </ul>
@@ -28,7 +28,7 @@
{% if emailprefix %} {% if emailprefix %}
<a id="add-email-helper" class="pure-button button-secondary button-xsmall" >Add email <img style="height: 1em; display: inline-block" src="{{url_for('static_content', group='images', filename='email.svg')}}" alt="Add an email address"> </a> <a id="add-email-helper" class="pure-button button-secondary button-xsmall" >Add email <img style="height: 1em; display: inline-block" src="{{url_for('static_content', group='images', filename='email.svg')}}" alt="Add an email address"> </a>
{% endif %} {% endif %}
<a href="{{url_for('notification_logs')}}" class="pure-button button-secondary button-xsmall" >Notification debug logs</a> <a href="{{url_for('settings.notification_logs')}}" class="pure-button button-secondary button-xsmall" >Notification debug logs</a>
<br> <br>
<div id="notification-test-log" style="display: none;"><span class="pure-form-message-inline">Processing..</span></div> <div id="notification-test-log" style="display: none;"><span class="pure-form-message-inline">Processing..</span></div>
</div> </div>
@@ -40,7 +40,7 @@
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }} {{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
<span class="pure-form-message-inline">Body for all notifications &dash; You can use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below. <span class="pure-form-message-inline">Body for all notifications &dash; You can use <a target="newwindow" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below.
</span> </span>
</div> </div>
@@ -126,7 +126,7 @@
<div class="pure-form-message-inline"> <div class="pure-form-message-inline">
<p> <p>
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br> Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br> For example, an addition or removal could be perceived as a change in some cases. <a target="newwindow" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
</p> </p>
<p> <p>
For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code> For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code>

View File

@@ -61,6 +61,55 @@
{{ field(**kwargs)|safe }} {{ field(**kwargs)|safe }}
{% endmacro %} {% endmacro %}
{% macro render_fieldlist_of_formfields_as_table(fieldlist, table_id="rulesTable") %}
<table class="fieldlist_formfields pure-table" id="{{ table_id }}">
<thead>
<tr>
{% for subfield in fieldlist[0] %}
<th>{{ subfield.label }}</th>
{% endfor %}
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for form_row in fieldlist %}
<tr {% if form_row.errors %} class="error-row" {% endif %}>
{% for subfield in form_row %}
<td>
{{ subfield()|safe }}
{% if subfield.errors %}
<ul class="errors">
{% for error in subfield.errors %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</td>
{% endfor %}
<td>
<button type="button" class="addRuleRow">+</button>
<button type="button" class="removeRuleRow">-</button>
<button type="button" class="verifyRuleRow" title="Verify this rule against current snapshot"></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
{% macro playwright_warning() %}
<p><strong>Error - Playwright support for Chrome based fetching is not enabled.</strong> Alternatively try our <a href="https://changedetection.io">very affordable subscription based service which has all this setup for you</a>.</p>
<p>You may need to <a href="https://github.com/dgtlmoon/changedetection.io/blob/09ebc6ec6338545bdd694dc6eee57f2e9d2b8075/docker-compose.yml#L31">Enable playwright environment variable</a> and uncomment the <strong>sockpuppetbrowser</strong> in the <a href="https://github.com/dgtlmoon/changedetection.io/blob/master/docker-compose.yml">docker-compose.yml</a> file.</p>
<br>
<p>(Also Selenium/WebDriver can not extract full page screenshots reliably so Playwright is recommended here)</p>
{% endmacro %}
{% macro only_webdriver_type_watches_warning() %}
<p><strong>Sorry, this functionality only works with Playwright/Chrome enabled watches.<br>You need to <a href="#request">Set the fetch method to Playwright/Chrome mode and resave</a> and have the Playwright connection enabled.</strong></p><br>
{% endmacro %}
{% macro render_time_schedule_form(form, available_timezones, timezone_default_config) %} {% macro render_time_schedule_form(form, available_timezones, timezone_default_config) %}
<style> <style>
.day-schedule *, .day-schedule select { .day-schedule *, .day-schedule select {
@@ -150,7 +199,7 @@
</div> </div>
{% else %} {% else %}
<span class="pure-form-message-inline"> <span class="pure-form-message-inline">
Want to use a time schedule? <a href="{{url_for('settings_page')}}#timedate">First confirm/save your Time Zone Settings</a> Want to use a time schedule? <a href="{{url_for('settings.settings_page')}}#timedate">First confirm/save your Time Zone Settings</a>
</span> </span>
<br> <br>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@
<meta name="description" content="Self hosted website change detection." > <meta name="description" content="Self hosted website change detection." >
<title>Change Detection{{extra_title}}</title> <title>Change Detection{{extra_title}}</title>
{% if app_rss_token %} {% if app_rss_token %}
<link rel="alternate" type="application/rss+xml" title="Changedetection.io » Feed{% if active_tag_uuid %}- {{active_tag.title}}{% endif %}" href="{{ url_for('rss', tag=active_tag_uuid , token=app_rss_token)}}" > <link rel="alternate" type="application/rss+xml" title="Changedetection.io » Feed{% if active_tag_uuid %}- {{active_tag.title}}{% endif %}" href="{{ url_for('rss.feed', tag=active_tag_uuid , token=app_rss_token)}}" >
{% endif %} {% endif %}
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='pure-min.css')}}" > <link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='pure-min.css')}}" >
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='styles.css')}}?v={{ get_css_version() }}" > <link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='styles.css')}}?v={{ get_css_version() }}" >
@@ -64,17 +64,17 @@
<a href="{{ url_for('tags.tags_overview_page')}}" class="pure-menu-link">GROUPS</a> <a href="{{ url_for('tags.tags_overview_page')}}" class="pure-menu-link">GROUPS</a>
</li> </li>
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('settings_page')}}" class="pure-menu-link">SETTINGS</a> <a href="{{ url_for('settings.settings_page')}}" class="pure-menu-link">SETTINGS</a>
</li> </li>
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('import_page')}}" class="pure-menu-link">IMPORT</a> <a href="{{ url_for('imports.import_page')}}" class="pure-menu-link">IMPORT</a>
</li> </li>
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('backups.index')}}" class="pure-menu-link">BACKUPS</a> <a href="{{ url_for('backups.index')}}" class="pure-menu-link">BACKUPS</a>
</li> </li>
{% else %} {% else %}
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('edit_page', uuid=uuid, next='diff') }}" class="pure-menu-link">EDIT</a> <a href="{{ url_for('ui.ui_edit.edit_page', uuid=uuid, next='diff') }}" class="pure-menu-link">EDIT</a>
</li> </li>
{% endif %} {% endif %}
{% else %} {% else %}
@@ -144,7 +144,7 @@
{% endif %} {% endif %}
{% if left_sticky %} {% if left_sticky %}
<div class="sticky-tab" id="left-sticky"> <div class="sticky-tab" id="left-sticky">
<a href="{{url_for('preview_page', uuid=uuid)}}">Show current snapshot</a><br> <a href="{{url_for('ui.ui_views.preview_page', uuid=uuid)}}">Show current snapshot</a><br>
Visualise <strong>triggers</strong> and <strong>ignored text</strong> Visualise <strong>triggers</strong> and <strong>ignored text</strong>
</div> </div>
{% endif %} {% endif %}
@@ -159,7 +159,7 @@
<a id="chrome-extension-link" <a id="chrome-extension-link"
title="Try our new Chrome Extension!" title="Try our new Chrome Extension!"
href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop"> href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop">
<img src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}"> <img alt="Chrome store icon" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}">
Chrome Webstore Chrome Webstore
</a> </a>
</p> </p>

View File

@@ -7,7 +7,7 @@
const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}"; const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
{% endif %} {% endif %}
const highlight_submit_ignore_url="{{url_for('highlight_submit_ignore_url', uuid=uuid)}}"; const highlight_submit_ignore_url="{{url_for('ui.ui_edit.highlight_submit_ignore_url', uuid=uuid)}}";
</script> </script>
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
@@ -125,7 +125,7 @@
</div> </div>
<div class="tab-pane-inner" id="extract"> <div class="tab-pane-inner" id="extract">
<form id="extract-data-form" class="pure-form pure-form-stacked edit-form" <form id="extract-data-form" class="pure-form pure-form-stacked edit-form"
action="{{ url_for('diff_history_page', uuid=uuid) }}#extract" action="{{ url_for('ui.ui_views.diff_history_page', uuid=uuid) }}#extract"
method="POST"> method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

View File

@@ -1,11 +1,14 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form %} {% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form, playwright_warning, only_webdriver_type_watches_warning, render_fieldlist_of_formfields_as_table %}
{% from '_common_fields.html' import render_common_settings_form %} {% from '_common_fields.html' import render_common_settings_form %}
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='global-settings.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='global-settings.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='scheduler.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='scheduler.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='conditions.js')}}" defer></script>
<script> <script>
const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}'); const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}');
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}'); const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
@@ -17,7 +20,7 @@
{% if emailprefix %} {% if emailprefix %}
const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}'); const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}');
{% endif %} {% endif %}
const notification_base_url="{{url_for('ajax_callback_send_notification_test', watch_uuid=uuid)}}"; const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', watch_uuid=uuid)}}";
const playwright_enabled={% if playwright_enabled %}true{% else %}false{% endif %}; const playwright_enabled={% if playwright_enabled %}true{% else %}false{% endif %};
const recheck_proxy_start_url="{{url_for('check_proxies.start_check', uuid=uuid)}}"; const recheck_proxy_start_url="{{url_for('check_proxies.start_check', uuid=uuid)}}";
const proxy_recheck_status_url="{{url_for('check_proxies.get_recheck_status', uuid=uuid)}}"; const proxy_recheck_status_url="{{url_for('check_proxies.get_recheck_status', uuid=uuid)}}";
@@ -40,17 +43,17 @@
<div class="tabs collapsable"> <div class="tabs collapsable">
<ul> <ul>
<li class="tab" id=""><a href="#general">General</a></li> <li class="tab"><a href="#general">General</a></li>
<li class="tab"><a href="#request">Request</a></li> <li class="tab"><a href="#request">Request</a></li>
{% if extra_tab_content %} {% if extra_tab_content %}
<li class="tab"><a href="#extras_tab">{{ extra_tab_content }}</a></li> <li class="tab"><a href="#extras_tab">{{ extra_tab_content }}</a></li>
{% endif %} {% endif %}
{% if playwright_enabled %}
<li class="tab"><a id="browsersteps-tab" href="#browser-steps">Browser Steps</a></li> <li class="tab"><a id="browsersteps-tab" href="#browser-steps">Browser Steps</a></li>
{% endif %} <!-- should goto extra forms? -->
{% if watch['processor'] == 'text_json_diff' %} {% if watch['processor'] == 'text_json_diff' %}
<li class="tab"><a id="visualselector-tab" href="#visualselector">Visual Filter Selector</a></li> <li class="tab"><a id="visualselector-tab" href="#visualselector">Visual Filter Selector</a></li>
<li class="tab" id="filters-and-triggers-tab"><a href="#filters-and-triggers">Filters &amp; Triggers</a></li> <li class="tab" id="filters-and-triggers-tab"><a href="#filters-and-triggers">Filters &amp; Triggers</a></li>
<li class="tab" id="conditions-tab"><a href="#conditions">Conditions</a></li>
{% endif %} {% endif %}
<li class="tab"><a href="#notifications">Notifications</a></li> <li class="tab"><a href="#notifications">Notifications</a></li>
<li class="tab"><a href="#stats">Stats</a></li> <li class="tab"><a href="#stats">Stats</a></li>
@@ -59,7 +62,7 @@
<div class="box-wrap inner"> <div class="box-wrap inner">
<form class="pure-form pure-form-stacked" <form class="pure-form pure-form-stacked"
action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next'), unpause_on_save = request.args.get('unpause_on_save'), tag = request.args.get('tag')) }}" method="POST"> action="{{ url_for('ui.ui_edit.edit_page', uuid=uuid, next = request.args.get('next'), unpause_on_save = request.args.get('unpause_on_save'), tag = request.args.get('tag')) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="tab-pane-inner" id="general"> <div class="tab-pane-inner" id="general">
@@ -199,8 +202,9 @@ Math: {{ 1 + 1 }}") }}
</div> </div>
</fieldset> </fieldset>
</div> </div>
{% if playwright_enabled %}
<div class="tab-pane-inner" id="browser-steps"> <div class="tab-pane-inner" id="browser-steps">
{% if playwright_enabled and watch_uses_webdriver %}
<img class="beta-logo" src="{{url_for('static_content', group='images', filename='beta-logo.png')}}" alt="New beta functionality"> <img class="beta-logo" src="{{url_for('static_content', group='images', filename='beta-logo.png')}}" alt="New beta functionality">
<fieldset> <fieldset>
<div class="pure-control-group"> <div class="pure-control-group">
@@ -224,7 +228,7 @@ Math: {{ 1 + 1 }}") }}
<span class="loader" > <span class="loader" >
<span id="browsersteps-click-start"> <span id="browsersteps-click-start">
<h2 >Click here to Start</h2> <h2 >Click here to Start</h2>
<svg style="height: 3.5rem;" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"/><g id="play_x5F_alt"><path d="M16,0C7.164,0,0,7.164,0,16s7.164,16,16,16s16-7.164,16-16S24.836,0,16,0z M10,24V8l16.008,8L10,24z" style="fill: var(--color-grey-400);"/></g></svg><br> <svg style="height: 3.5rem;" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="start"/><g id="play_x5F_alt"><path d="M16,0C7.164,0,0,7.164,0,16s7.164,16,16,16s16-7.164,16-16S24.836,0,16,0z M10,24V8l16.008,8L10,24z" style="fill: var(--color-grey-400);"/></g></svg><br>
Please allow 10-15 seconds for the browser to connect.<br> Please allow 10-15 seconds for the browser to connect.<br>
</span> </span>
<div class="spinner" style="display: none;"></div> <div class="spinner" style="display: none;"></div>
@@ -234,21 +238,31 @@ Math: {{ 1 + 1 }}") }}
</div> </div>
</div> </div>
<div id="browser-steps-fieldlist" > <div id="browser-steps-fieldlist" >
<span id="browser-seconds-remaining">Loading</span> <span style="font-size: 80%;"> (<a target=_new href="https://github.com/dgtlmoon/changedetection.io/pull/478/files#diff-1a79d924d1840c485238e66772391268a89c95b781d69091384cf1ea1ac146c9R4">?</a>) </span> <span id="browser-seconds-remaining">Loading</span> <span style="font-size: 80%;"> (<a target="newwindow" href="https://github.com/dgtlmoon/changedetection.io/pull/478/files#diff-1a79d924d1840c485238e66772391268a89c95b781d69091384cf1ea1ac146c9R4">?</a>) </span>
{{ render_field(form.browser_steps) }} {{ render_field(form.browser_steps) }}
</div> </div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
{% else %}
<span class="pure-form-message-inline">
{% if not watch_uses_webdriver %}
{{ only_webdriver_type_watches_warning() }}
{% endif %}
{% if not playwright_enabled %}
{{ playwright_warning() }}
{% endif %}
</span>
{% endif %}
</div> </div>
{% endif %}
<div class="tab-pane-inner" id="notifications"> <div class="tab-pane-inner" id="notifications">
<fieldset> <fieldset>
<div class="pure-control-group inline-radio"> <div class="pure-control-group inline-radio">
{{ render_checkbox_field(form.notification_muted) }} {{ render_checkbox_field(form.notification_muted) }}
</div> </div>
{% if is_html_webdriver %} {% if watch_uses_webdriver %}
<div class="pure-control-group inline-radio"> <div class="pure-control-group inline-radio">
{{ render_checkbox_field(form.notification_screenshot) }} {{ render_checkbox_field(form.notification_screenshot) }}
<span class="pure-form-message-inline"> <span class="pure-form-message-inline">
@@ -260,17 +274,43 @@ Math: {{ 1 + 1 }}") }}
{% if has_default_notification_urls %} {% if has_default_notification_urls %}
<div class="inline-warning"> <div class="inline-warning">
<img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="Look out!" title="Lookout!" > <img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="Look out!" title="Lookout!" >
There are <a href="{{ url_for('settings_page')}}#notifications">system-wide notification URLs enabled</a>, this form will override notification settings for this watch only &dash; an empty Notification URL list here will still send notifications. There are <a href="{{ url_for('settings.settings_page')}}#notifications">system-wide notification URLs enabled</a>, this form will override notification settings for this watch only &dash; an empty Notification URL list here will still send notifications.
</div> </div>
{% endif %} {% endif %}
<a href="#notifications" id="notification-setting-reset-to-default" class="pure-button button-xsmall" style="right: 20px; top: 20px; position: absolute; background-color: #5f42dd; border-radius: 4px; font-size: 70%; color: #fff">Use system defaults</a> <a href="#notifications" id="notification-setting-reset-to-default" class="pure-button button-xsmall" style="right: 20px; top: 20px; position: absolute; background-color: #5f42dd; border-radius: 4px; font-size: 70%; color: #fff">Use system defaults</a>
{{ render_common_settings_form(form, emailprefix, settings_application, extra_notification_token_placeholder_info) }} {{ render_common_settings_form(form, emailprefix, settings_application, extra_notification_token_placeholder_info) }}
</div> </div>
</fieldset> </fieldset>
</div> </div>
{% if watch['processor'] == 'text_json_diff' %} {% if watch['processor'] == 'text_json_diff' %}
<div class="tab-pane-inner" id="conditions">
<script>
const verify_condition_rule_url="{{url_for('conditions.verify_condition_single_rule', watch_uuid=uuid)}}";
</script>
<style>
.verifyRuleRow {
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
font-weight: bold;
}
.verifyRuleRow:hover {
background-color: #45a049;
}
</style>
<div class="pure-control-group">
{{ render_field(form.conditions_match_logic) }}
{{ render_fieldlist_of_formfields_as_table(form.conditions) }}
<div class="pure-form-message-inline">
<br>
Use the verify (✓) button to test if a condition passes against the current snapshot.<br><br>
Did you know that <strong>conditions</strong> can be extended with your own custom plugin? tutorials coming soon!<br>
</div>
</div>
</div>
<div class="tab-pane-inner" id="filters-and-triggers"> <div class="tab-pane-inner" id="filters-and-triggers">
<span id="activate-text-preview" class="pure-button pure-button-primary button-xsmall">Activate preview</span> <span id="activate-text-preview" class="pure-button pure-button-primary button-xsmall">Activate preview</span>
<div> <div>
@@ -298,7 +338,7 @@ xpath://body/div/span[contains(@class, 'example-class')]",
<span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the &lt;element&gt; contains &lt;![CDATA[]]&gt;</strong></span><br> <span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the &lt;element&gt; contains &lt;![CDATA[]]&gt;</strong></span><br>
{% endif %} {% endif %}
<span class="pure-form-message-inline">One CSS, xPath, JSON Path/JQ selector per line, <i>any</i> rules that matches will be used.<br> <span class="pure-form-message-inline">One CSS, xPath, JSON Path/JQ selector per line, <i>any</i> rules that matches will be used.<br>
<p><div data-target="#advanced-help-selectors" class="toggle-show pure-button button-tag button-xsmall">Show advanced help and tips</div><br></p> <span data-target="#advanced-help-selectors" class="toggle-show pure-button button-tag button-xsmall">Show advanced help and tips</span><br>
<ul id="advanced-help-selectors" style="display: none;"> <ul id="advanced-help-selectors" style="display: none;">
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li> <li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
<li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed). <li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed).
@@ -440,7 +480,7 @@ keyword") }}
</div> </div>
<div id="text-preview" style="display: none;" > <div id="text-preview" style="display: none;" >
<script> <script>
const preview_text_edit_filters_url="{{url_for('watch_get_preview_rendered', uuid=uuid)}}"; const preview_text_edit_filters_url="{{url_for('ui.ui_edit.watch_get_preview_rendered', uuid=uuid)}}";
</script> </script>
<br> <br>
{#<div id="text-preview-controls"><span id="text-preview-refresh" class="pure-button button-xsmall">Refresh</span></div>#} {#<div id="text-preview-controls"><span id="text-preview-refresh" class="pure-button button-xsmall">Refresh</span></div>#}
@@ -471,7 +511,7 @@ keyword") }}
<fieldset> <fieldset>
<div class="pure-control-group"> <div class="pure-control-group">
{% if visualselector_enabled %} {% if playwright_enabled and watch_uses_webdriver %}
<span class="pure-form-message-inline" id="visual-selector-heading"> <span class="pure-form-message-inline" id="visual-selector-heading">
The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection. It automatically fills-in the filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab. Use <strong>Shift+Click</strong> to select multiple items. The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection. It automatically fills-in the filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab. Use <strong>Shift+Click</strong> to select multiple items.
</span> </span>
@@ -489,11 +529,12 @@ keyword") }}
</div> </div>
<div id="selector-current-xpath" style="overflow-x: hidden"><strong>Currently:</strong>&nbsp;<span class="text">Loading...</span></div> <div id="selector-current-xpath" style="overflow-x: hidden"><strong>Currently:</strong>&nbsp;<span class="text">Loading...</span></div>
{% else %} {% else %}
<span class="pure-form-message-inline"> {% if not watch_uses_webdriver %}
<p>Sorry, this functionality only works with Playwright/Chrome enabled watches.</p> {{ only_webdriver_type_watches_warning() }}
<p>Enable the Playwright Chrome fetcher, or alternatively try our <a href="https://lemonade.changedetection.io/start">very affordable subscription based service</a>.</p> {% endif %}
<p>This is because Selenium/WebDriver can not extract full page screenshots reliably.</p> {% if not playwright_enabled %}
</span> {{ playwright_warning() }}
{% endif %}
{% endif %} {% endif %}
</div> </div>
</fieldset> </fieldset>
@@ -536,7 +577,7 @@ keyword") }}
</table> </table>
{% if watch.history_n %} {% if watch.history_n %}
<p> <p>
<a href="{{url_for('watch_get_latest_html', uuid=uuid)}}" class="pure-button button-small">Download latest HTML snapshot</a> <a href="{{url_for('ui.ui_edit.watch_get_latest_html', uuid=uuid)}}" class="pure-button button-small">Download latest HTML snapshot</a>
</p> </p>
{% endif %} {% endif %}
@@ -545,11 +586,11 @@ keyword") }}
<div id="actions"> <div id="actions">
<div class="pure-control-group"> <div class="pure-control-group">
{{ render_button(form.save_button) }} {{ render_button(form.save_button) }}
<a href="{{url_for('form_delete', uuid=uuid)}}" <a href="{{url_for('ui.form_delete', uuid=uuid)}}"
class="pure-button button-small button-error ">Delete</a> class="pure-button button-small button-error ">Delete</a>
<a href="{{url_for('clear_watch_history', uuid=uuid)}}" <a href="{{url_for('ui.clear_watch_history', uuid=uuid)}}"
class="pure-button button-small button-error ">Clear History</a> class="pure-button button-small button-error ">Clear History</a>
<a href="{{url_for('form_clone', uuid=uuid)}}" <a href="{{url_for('ui.form_clone', uuid=uuid)}}"
class="pure-button button-small ">Create Copy</a> class="pure-button button-small ">Create Copy</a>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@
{% if last_error_screenshot %} {% if last_error_screenshot %}
const error_screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}"; const error_screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
{% endif %} {% endif %}
const highlight_submit_ignore_url = "{{url_for('highlight_submit_ignore_url', uuid=uuid)}}"; const highlight_submit_ignore_url = "{{url_for('ui.ui_edit.highlight_submit_ignore_url', uuid=uuid)}}";
</script> </script>
<script src="{{url_for('static_content', group='js', filename='plugins.js')}}"></script> <script src="{{url_for('static_content', group='js', filename='plugins.js')}}"></script>
<script src="{{ url_for('static_content', group='js', filename='diff-overview.js') }}" defer></script> <script src="{{ url_for('static_content', group='js', filename='diff-overview.js') }}" defer></script>

View File

@@ -1 +1 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 122.879 119.799" enable-background="new 0 0 122.879 119.799" xml:space="preserve"><g><path d="M49.988,0h0.016v0.007C63.803,0.011,76.298,5.608,85.34,14.652c9.027,9.031,14.619,21.515,14.628,35.303h0.007v0.033v0.04 h-0.007c-0.005,5.557-0.917,10.905-2.594,15.892c-0.281,0.837-0.575,1.641-0.877,2.409v0.007c-1.446,3.66-3.315,7.12-5.547,10.307 l29.082,26.139l0.018,0.016l0.157,0.146l0.011,0.011c1.642,1.563,2.536,3.656,2.649,5.78c0.11,2.1-0.543,4.248-1.979,5.971 l-0.011,0.016l-0.175,0.203l-0.035,0.035l-0.146,0.16l-0.016,0.021c-1.565,1.642-3.654,2.534-5.78,2.646 c-2.097,0.111-4.247-0.54-5.971-1.978l-0.015-0.011l-0.204-0.175l-0.029-0.024L78.761,90.865c-0.88,0.62-1.778,1.209-2.687,1.765 c-1.233,0.755-2.51,1.466-3.813,2.115c-6.699,3.342-14.269,5.222-22.272,5.222v0.007h-0.016v-0.007 c-13.799-0.004-26.296-5.601-35.338-14.645C5.605,76.291,0.016,63.805,0.007,50.021H0v-0.033v-0.016h0.007 c0.004-13.799,5.601-26.296,14.645-35.338C23.683,5.608,36.167,0.016,49.955,0.007V0H49.988L49.988,0z M50.004,11.21v0.007h-0.016 h-0.033V11.21c-10.686,0.007-20.372,4.35-27.384,11.359C15.56,29.578,11.213,39.274,11.21,49.973h0.007v0.016v0.033H11.21 c0.007,10.686,4.347,20.367,11.359,27.381c7.009,7.012,16.705,11.359,27.403,11.361v-0.007h0.016h0.033v0.007 c10.686-0.007,20.368-4.348,27.382-11.359c7.011-7.009,11.358-16.702,11.36-27.4h-0.006v-0.016v-0.033h0.006 c-0.006-10.686-4.35-20.372-11.358-27.384C70.396,15.56,60.703,11.213,50.004,11.21L50.004,11.21z"/></g></svg> <svg version="1.1" id="search" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 122.879 119.799" enable-background="new 0 0 122.879 119.799" xml:space="preserve"><g><path d="M49.988,0h0.016v0.007C63.803,0.011,76.298,5.608,85.34,14.652c9.027,9.031,14.619,21.515,14.628,35.303h0.007v0.033v0.04 h-0.007c-0.005,5.557-0.917,10.905-2.594,15.892c-0.281,0.837-0.575,1.641-0.877,2.409v0.007c-1.446,3.66-3.315,7.12-5.547,10.307 l29.082,26.139l0.018,0.016l0.157,0.146l0.011,0.011c1.642,1.563,2.536,3.656,2.649,5.78c0.11,2.1-0.543,4.248-1.979,5.971 l-0.011,0.016l-0.175,0.203l-0.035,0.035l-0.146,0.16l-0.016,0.021c-1.565,1.642-3.654,2.534-5.78,2.646 c-2.097,0.111-4.247-0.54-5.971-1.978l-0.015-0.011l-0.204-0.175l-0.029-0.024L78.761,90.865c-0.88,0.62-1.778,1.209-2.687,1.765 c-1.233,0.755-2.51,1.466-3.813,2.115c-6.699,3.342-14.269,5.222-22.272,5.222v0.007h-0.016v-0.007 c-13.799-0.004-26.296-5.601-35.338-14.645C5.605,76.291,0.016,63.805,0.007,50.021H0v-0.033v-0.016h0.007 c0.004-13.799,5.601-26.296,14.645-35.338C23.683,5.608,36.167,0.016,49.955,0.007V0H49.988L49.988,0z M50.004,11.21v0.007h-0.016 h-0.033V11.21c-10.686,0.007-20.372,4.35-27.384,11.359C15.56,29.578,11.213,39.274,11.21,49.973h0.007v0.016v0.033H11.21 c0.007,10.686,4.347,20.367,11.359,27.381c7.009,7.012,16.705,11.359,27.403,11.361v-0.007h0.016h0.033v0.007 c10.686-0.007,20.368-4.348,27.382-11.359c7.011-7.009,11.358-16.702,11.36-27.4h-0.006v-0.016v-0.033h0.006 c-0.006-10.686-4.35-20.372-11.358-27.384C70.396,15.56,60.703,11.213,50.004,11.21L50.004,11.21z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -6,7 +6,7 @@
<div class="box"> <div class="box">
<form class="pure-form" action="{{ url_for('form_quick_watch_add', tag=active_tag_uuid) }}" method="POST" id="new-watch-form"> <form class="pure-form" action="{{ url_for('ui.ui_views.form_quick_watch_add', tag=active_tag_uuid) }}" method="POST" id="new-watch-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" > <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >
<fieldset> <fieldset>
<legend>Add a new change detection watch</legend> <legend>Add a new change detection watch</legend>
@@ -25,7 +25,7 @@
<span style="color:#eee; font-size: 80%;"><img alt="Create a shareable link" style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread-white.svg')}}" > Tip: You can also add 'shared' watches. <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Sharing-a-Watch">More info</a></span> <span style="color:#eee; font-size: 80%;"><img alt="Create a shareable link" style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread-white.svg')}}" > Tip: You can also add 'shared' watches. <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Sharing-a-Watch">More info</a></span>
</form> </form>
<form class="pure-form" action="{{ url_for('form_watch_list_checkbox_operations') }}" method="POST" id="watch-list-form"> <form class="pure-form" action="{{ url_for('ui.form_watch_list_checkbox_operations') }}" method="POST" id="watch-list-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" > <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" >
<input type="hidden" id="op_extradata" name="op_extradata" value="" > <input type="hidden" id="op_extradata" name="op_extradata" value="" >
<div id="checkbox-operations"> <div id="checkbox-operations">
@@ -86,7 +86,7 @@
<tbody> <tbody>
{% if not watches|length %} {% if not watches|length %}
<tr> <tr>
<td colspan="{{ cols_required }}" style="text-wrap: wrap;">No website watches configured, please add a URL in the box above, or <a href="{{ url_for('import_page')}}" >import a list</a>.</td> <td colspan="{{ cols_required }}" style="text-wrap: wrap;">No website watches configured, please add a URL in the box above, or <a href="{{ url_for('imports.import_page')}}" >import a list</a>.</td>
</tr> </tr>
{% endif %} {% endif %}
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %} {% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
@@ -108,17 +108,18 @@
{% else %} {% else %}
<a class="state-on" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a> <a class="state-on" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a>
{% endif %} {% endif %}
<a class="link-mute state-{{'on' if watch.notification_muted else 'off'}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a> {% set mute_label = 'UnMute notification' if watch.notification_muted else 'Mute notification' %}
<a class="link-mute state-{{'on' if watch.notification_muted else 'off'}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="{{ mute_label }}" title="{{ mute_label }}" class="icon icon-mute" ></a>
</td> </td>
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}} <td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a> <a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
<a class="link-spread" href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="status-icon icon icon-spread" title="Create a link to share watch config with others" ></a> <a class="link-spread" href="{{url_for('ui.form_share_put_watch', uuid=watch.uuid)}}"><img src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="status-icon icon icon-spread" title="Create a link to share watch config with others" ></a>
{% if watch.get_fetch_backend == "html_webdriver" {% if watch.get_fetch_backend == "html_webdriver"
or ( watch.get_fetch_backend == "system" and system_default_fetcher == 'html_webdriver' ) or ( watch.get_fetch_backend == "system" and system_default_fetcher == 'html_webdriver' )
or "extra_browser_" in watch.get_fetch_backend or "extra_browser_" in watch.get_fetch_backend
%} %}
<img class="status-icon" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" title="Using a Chrome browser" > <img class="status-icon" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" alt="Using a Chrome browser" title="Using a Chrome browser" >
{% endif %} {% endif %}
{%if watch.is_pdf %}<img class="status-icon" src="{{url_for('static_content', group='images', filename='pdf-icon.svg')}}" title="Converting PDF to text" >{% endif %} {%if watch.is_pdf %}<img class="status-icon" src="{{url_for('static_content', group='images', filename='pdf-icon.svg')}}" title="Converting PDF to text" >{% endif %}
@@ -128,9 +129,9 @@
{% if '403' in watch.last_error %} {% if '403' in watch.last_error %}
{% if has_proxies %} {% if has_proxies %}
<a href="{{ url_for('settings_page', uuid=watch.uuid) }}#proxies">Try other proxies/location</a>&nbsp; <a href="{{ url_for('settings.settings_page', uuid=watch.uuid) }}#proxies">Try other proxies/location</a>&nbsp;
{% endif %} {% endif %}
<a href="{{ url_for('settings_page', uuid=watch.uuid) }}#proxies">Try adding external proxies/locations</a> <a href="{{ url_for('settings.settings_page', uuid=watch.uuid) }}#proxies">Try adding external proxies/locations</a>
{% endif %} {% endif %}
{% if 'empty result or contain only an image' in watch.last_error %} {% if 'empty result or contain only an image' in watch.last_error %}
@@ -139,7 +140,7 @@
</div> </div>
{% endif %} {% endif %}
{% if watch.last_notification_error is defined and watch.last_notification_error != False %} {% if watch.last_notification_error is defined and watch.last_notification_error != False %}
<div class="fetch-error notification-error"><a href="{{url_for('notification_logs')}}">{{ watch.last_notification_error }}</a></div> <div class="fetch-error notification-error"><a href="{{url_for('settings.notification_logs')}}">{{ watch.last_notification_error }}</a></div>
{% endif %} {% endif %}
{% if watch['processor'] == 'text_json_diff' %} {% if watch['processor'] == 'text_json_diff' %}
@@ -185,20 +186,20 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}" <a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}"
class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a> class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a>
<a href="{{ url_for('edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a> <a href="{{ url_for('ui.ui_edit.edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
{% if watch.history_n >= 2 %} {% if watch.history_n >= 2 %}
{% if is_unviewed %} {% if is_unviewed %}
<a href="{{ url_for('diff_history_page', uuid=watch.uuid, from_version=watch.get_from_version_based_on_last_viewed) }}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a> <a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid, from_version=watch.get_from_version_based_on_last_viewed) }}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a>
{% else %} {% else %}
<a href="{{ url_for('diff_history_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a> <a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a>
{% endif %} {% endif %}
{% else %} {% else %}
{% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%} {% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%}
<a href="{{ url_for('preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary">Preview</a> <a href="{{ url_for('ui.ui_views.preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary">Preview</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>
@@ -214,15 +215,15 @@
{% endif %} {% endif %}
{% if has_unviewed %} {% if has_unviewed %}
<li> <li>
<a href="{{url_for('mark_all_viewed',with_errors=request.args.get('with_errors',0)) }}" class="pure-button button-tag ">Mark all viewed</a> <a href="{{url_for('ui.mark_all_viewed',with_errors=request.args.get('with_errors',0)) }}" class="pure-button button-tag ">Mark all viewed</a>
</li> </li>
{% endif %} {% endif %}
<li> <li>
<a href="{{ url_for('form_watch_checknow', tag=active_tag_uuid, with_errors=request.args.get('with_errors',0)) }}" class="pure-button button-tag ">Recheck <a href="{{ url_for('ui.form_watch_checknow', tag=active_tag_uuid, with_errors=request.args.get('with_errors',0)) }}" class="pure-button button-tag ">Recheck
all {% if active_tag_uuid %} in "{{active_tag.title}}"{%endif%}</a> all {% if active_tag_uuid %} in "{{active_tag.title}}"{%endif%}</a>
</li> </li>
<li> <li>
<a href="{{ url_for('rss', tag=active_tag_uuid, token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='Generic_Feed-icon.svg')}}" height="15"></a> <a href="{{ url_for('rss.feed', tag=active_tag_uuid, token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='Generic_Feed-icon.svg')}}" height="15"></a>
</li> </li>
</ul> </ul>
{{ pagination.links }} {{ pagination.links }}

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import resource import psutil
import time import time
from threading import Thread from threading import Thread
@@ -28,9 +28,10 @@ def reportlog(pytestconfig):
def track_memory(memory_usage, ): def track_memory(memory_usage, ):
process = psutil.Process(os.getpid())
while not memory_usage["stop"]: while not memory_usage["stop"]:
max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss current_rss = process.memory_info().rss
memory_usage["peak"] = max(memory_usage["peak"], max_rss) memory_usage["peak"] = max(memory_usage["peak"], current_rss)
time.sleep(0.01) # Adjust the sleep time as needed time.sleep(0.01) # Adjust the sleep time as needed
@pytest.fixture(scope='function') @pytest.fixture(scope='function')

View File

@@ -16,7 +16,7 @@ def do_test(client, live_server, make_test_use_extra_browser=False):
##################### #####################
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-empty_pages_are_a_change": "", data={"application-empty_pages_are_a_change": "",
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_webdriver", 'application-fetch_backend': "html_webdriver",
@@ -30,7 +30,7 @@ def do_test(client, live_server, make_test_use_extra_browser=False):
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -42,13 +42,13 @@ def do_test(client, live_server, make_test_use_extra_browser=False):
# So the name should appear in the edit page under "Request" > "Fetch Method" # So the name should appear in the edit page under "Request" > "Fetch Method"
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
assert b'custom browser URL' in res.data assert b'custom browser URL' in res.data
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={ data={
# 'run_customer_browser_url_tests.sh' will search for this string to know if we hit the right browser container or not # 'run_customer_browser_url_tests.sh' will search for this string to know if we hit the right browser container or not
"url": f"https://changedetection.io/ci-test.html?custom-browser-search-string=1", "url": f"https://changedetection.io/ci-test.html?custom-browser-search-string=1",
@@ -64,13 +64,13 @@ def do_test(client, live_server, make_test_use_extra_browser=False):
wait_for_all_checks(client) wait_for_all_checks(client)
# Force recheck # Force recheck
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
assert b'cool it works' in res.data assert b'cool it works' in res.data

View File

@@ -11,7 +11,7 @@ def test_fetch_webdriver_content(client, live_server, measure_memory_usage):
##################### #####################
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-empty_pages_are_a_change": "", data={"application-empty_pages_are_a_change": "",
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_webdriver"}, 'application-fetch_backend': "html_webdriver"},
@@ -22,7 +22,7 @@ def test_fetch_webdriver_content(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": "https://changedetection.io/ci-test.html"}, data={"urls": "https://changedetection.io/ci-test.html"},
follow_redirects=True follow_redirects=True
) )
@@ -32,7 +32,7 @@ def test_fetch_webdriver_content(client, live_server, measure_memory_usage):
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
logging.getLogger().info("Looking for correct fetched HTML (text) from server") logging.getLogger().info("Looking for correct fetched HTML (text) from server")

View File

@@ -13,7 +13,7 @@ def test_execute_custom_js(client, live_server, measure_memory_usage):
test_url = test_url.replace('localhost', 'cdio') test_url = test_url.replace('localhost', 'cdio')
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
follow_redirects=True follow_redirects=True
) )
@@ -21,7 +21,7 @@ def test_execute_custom_js(client, live_server, measure_memory_usage):
assert b"Watch added in Paused state, saving will unpause" in res.data assert b"Watch added in Paused state, saving will unpause" in res.data
res = client.post( res = client.post(
url_for("edit_page", uuid="first", unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1),
data={ data={
"url": test_url, "url": test_url,
"tags": "", "tags": "",
@@ -34,14 +34,14 @@ def test_execute_custom_js(client, live_server, measure_memory_usage):
assert b"unpaused" in res.data assert b"unpaused" in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
assert live_server.app.config['DATASTORE'].data['watching'][uuid].history_n >= 1, "Watch history had atleast 1 (everything fetched OK)" assert live_server.app.config['DATASTORE'].data['watching'][uuid].history_n >= 1, "Watch history had atleast 1 (everything fetched OK)"
assert b"This text should be removed" not in res.data assert b"This text should be removed" not in res.data
# Check HTML conversion detected and workd # Check HTML conversion detected and workd
res = client.get( res = client.get(
url_for("preview_page", uuid=uuid), url_for("ui.ui_views.preview_page", uuid=uuid),
follow_redirects=True follow_redirects=True
) )
assert b"This text should be removed" not in res.data assert b"This text should be removed" not in res.data
@@ -51,6 +51,6 @@ def test_execute_custom_js(client, live_server, measure_memory_usage):
assert b"user-agent: mycustomagent" in res.data assert b"user-agent: mycustomagent" in res.data
client.get( client.get(
url_for("form_delete", uuid="all"), url_for("ui.form_delete", uuid="all"),
follow_redirects=True follow_redirects=True
) )

View File

@@ -11,7 +11,7 @@ def test_preferred_proxy(client, live_server, measure_memory_usage):
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, data={"url": url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
follow_redirects=True follow_redirects=True
) )
@@ -19,7 +19,7 @@ def test_preferred_proxy(client, live_server, measure_memory_usage):
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.post( res = client.post(
url_for("edit_page", uuid="first", unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1),
data={ data={
"include_filters": "", "include_filters": "",
"fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests', "fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests',

View File

@@ -13,12 +13,12 @@ def test_noproxy_option(client, live_server, measure_memory_usage):
# Should only be available when a proxy is setup # Should only be available when a proxy is setup
res = client.get( res = client.get(
url_for("edit_page", uuid="first", unpause_on_save=1)) url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1))
assert b'No proxy' not in res.data assert b'No proxy' not in res.data
# Setup a proxy # Setup a proxy
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-ignore_whitespace": "y", "application-ignore_whitespace": "y",
@@ -37,24 +37,24 @@ def test_noproxy_option(client, live_server, measure_memory_usage):
# Should be available as an option # Should be available as an option
res = client.get( res = client.get(
url_for("settings_page", unpause_on_save=1)) url_for("settings.settings_page", unpause_on_save=1))
assert b'No proxy' in res.data assert b'No proxy' in res.data
# This will add it paused # This will add it paused
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, data={"url": url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
follow_redirects=True follow_redirects=True
) )
assert b"Watch added in Paused state, saving will unpause" in res.data assert b"Watch added in Paused state, saving will unpause" in res.data
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
res = client.get( res = client.get(
url_for("edit_page", uuid=uuid, unpause_on_save=1)) url_for("ui.ui_edit.edit_page", uuid=uuid, unpause_on_save=1))
assert b'No proxy' in res.data assert b'No proxy' in res.data
res = client.post( res = client.post(
url_for("edit_page", uuid=uuid, unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid=uuid, unpause_on_save=1),
data={ data={
"include_filters": "", "include_filters": "",
"fetch_backend": "html_requests", "fetch_backend": "html_requests",
@@ -67,7 +67,7 @@ def test_noproxy_option(client, live_server, measure_memory_usage):
) )
assert b"unpaused" in res.data assert b"unpaused" in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
# Now the request should NOT appear in the second-squid logs (handled by the run_test_proxies.sh script) # Now the request should NOT appear in the second-squid logs (handled by the run_test_proxies.sh script)

View File

@@ -8,7 +8,7 @@ from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_cli
def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage): def test_check_basic_change_detection_functionality(client, live_server, measure_memory_usage):
live_server_setup(live_server) live_server_setup(live_server)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
# Because a URL wont show in squid/proxy logs due it being SSLed # Because a URL wont show in squid/proxy logs due it being SSLed
# Use plain HTTP or a specific domain-name here # Use plain HTTP or a specific domain-name here
data={"urls": "http://one.changedetection.io"}, data={"urls": "http://one.changedetection.io"},

View File

@@ -11,7 +11,7 @@ def test_select_custom(client, live_server, measure_memory_usage):
# Goto settings, add our custom one # Goto settings, add our custom one
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-ignore_whitespace": "y", "application-ignore_whitespace": "y",
@@ -26,7 +26,7 @@ def test_select_custom(client, live_server, measure_memory_usage):
assert b"Settings updated." in res.data assert b"Settings updated." in res.data
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
# Because a URL wont show in squid/proxy logs due it being SSLed # Because a URL wont show in squid/proxy logs due it being SSLed
# Use plain HTTP or a specific domain-name here # Use plain HTTP or a specific domain-name here
data={"urls": "https://changedetection.io/CHANGELOG.txt"}, data={"urls": "https://changedetection.io/CHANGELOG.txt"},
@@ -40,7 +40,7 @@ def test_select_custom(client, live_server, measure_memory_usage):
assert b'Proxy Authentication Required' not in res.data assert b'Proxy Authentication Required' not in res.data
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
# We should see something via proxy # We should see something via proxy

View File

@@ -25,7 +25,7 @@ def test_socks5(client, live_server, measure_memory_usage):
# Setup a proxy # Setup a proxy
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-ignore_whitespace": "y", "application-ignore_whitespace": "y",
@@ -45,20 +45,20 @@ def test_socks5(client, live_server, measure_memory_usage):
test_url = test_url.replace('localhost', 'cdio') test_url = test_url.replace('localhost', 'cdio')
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
follow_redirects=True follow_redirects=True
) )
assert b"Watch added in Paused state, saving will unpause" in res.data assert b"Watch added in Paused state, saving will unpause" in res.data
res = client.get( res = client.get(
url_for("edit_page", uuid="first", unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1),
) )
# check the proxy is offered as expected # check the proxy is offered as expected
assert b'ui-0socks5proxy' in res.data assert b'ui-0socks5proxy' in res.data
res = client.post( res = client.post(
url_for("edit_page", uuid="first", unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1),
data={ data={
"include_filters": "", "include_filters": "",
"fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests', "fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests',
@@ -73,7 +73,7 @@ def test_socks5(client, live_server, measure_memory_usage):
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
@@ -81,7 +81,7 @@ def test_socks5(client, live_server, measure_memory_usage):
assert "Awesome, you made it".encode('utf-8') in res.data assert "Awesome, you made it".encode('utf-8') in res.data
# PROXY CHECKER WIDGET CHECK - this needs more checking # PROXY CHECKER WIDGET CHECK - this needs more checking
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
res = client.get( res = client.get(
url_for("check_proxies.start_check", uuid=uuid), url_for("check_proxies.start_check", uuid=uuid),
@@ -97,6 +97,6 @@ def test_socks5(client, live_server, measure_memory_usage):
) )
assert b"OK" in res.data assert b"OK" in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -28,24 +28,24 @@ def test_socks5_from_proxiesjson_file(client, live_server, measure_memory_usage)
test_url = test_url.replace('localhost.localdomain', 'cdio') test_url = test_url.replace('localhost.localdomain', 'cdio')
test_url = test_url.replace('localhost', 'cdio') test_url = test_url.replace('localhost', 'cdio')
res = client.get(url_for("settings_page")) res = client.get(url_for("settings.settings_page"))
assert b'name="requests-proxy" type="radio" value="socks5proxy"' in res.data assert b'name="requests-proxy" type="radio" value="socks5proxy"' in res.data
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'}, data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
follow_redirects=True follow_redirects=True
) )
assert b"Watch added in Paused state, saving will unpause" in res.data assert b"Watch added in Paused state, saving will unpause" in res.data
res = client.get( res = client.get(
url_for("edit_page", uuid="first", unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1),
) )
# check the proxy is offered as expected # check the proxy is offered as expected
assert b'name="proxy" type="radio" value="socks5proxy"' in res.data assert b'name="proxy" type="radio" value="socks5proxy"' in res.data
res = client.post( res = client.post(
url_for("edit_page", uuid="first", unpause_on_save=1), url_for("ui.ui_edit.edit_page", uuid="first", unpause_on_save=1),
data={ data={
"include_filters": "", "include_filters": "",
"fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests', "fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests',
@@ -60,7 +60,7 @@ def test_socks5_from_proxiesjson_file(client, live_server, measure_memory_usage)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )

View File

@@ -62,7 +62,7 @@ def test_restock_detection(client, live_server, measure_memory_usage):
##################### #####################
# Set this up for when we remove the notification from the watch, it should fallback with these details # Set this up for when we remove the notification from the watch, it should fallback with these details
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-notification_urls": notification_url, data={"application-notification_urls": notification_url,
"application-notification_title": "fallback-title "+default_notification_title, "application-notification_title": "fallback-title "+default_notification_title,
"application-notification_body": "fallback-body "+default_notification_body, "application-notification_body": "fallback-body "+default_notification_body,
@@ -76,7 +76,7 @@ def test_restock_detection(client, live_server, measure_memory_usage):
client.post( client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": '', 'processor': 'restock_diff'}, data={"url": test_url, "tags": '', 'processor': 'restock_diff'},
follow_redirects=True follow_redirects=True
) )
@@ -88,7 +88,7 @@ def test_restock_detection(client, live_server, measure_memory_usage):
# Is it correctly shown as in stock # Is it correctly shown as in stock
set_back_in_stock_response() set_back_in_stock_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'not-in-stock' not in res.data assert b'not-in-stock' not in res.data
@@ -101,7 +101,7 @@ def test_restock_detection(client, live_server, measure_memory_usage):
# Default behaviour is to only fire notification when it goes OUT OF STOCK -> IN STOCK # Default behaviour is to only fire notification when it goes OUT OF STOCK -> IN STOCK
# So here there should be no file, because we go IN STOCK -> OUT OF STOCK # So here there should be no file, because we go IN STOCK -> OUT OF STOCK
set_original_response() set_original_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
time.sleep(5) time.sleep(5)
assert not os.path.isfile("test-datastore/notification.txt"), "No notification should have fired when it went OUT OF STOCK by default" assert not os.path.isfile("test-datastore/notification.txt"), "No notification should have fired when it went OUT OF STOCK by default"

View File

@@ -50,7 +50,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas
##################### #####################
# Set this up for when we remove the notification from the watch, it should fallback with these details # Set this up for when we remove the notification from the watch, it should fallback with these details
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-notification_urls": notification_url, data={"application-notification_urls": notification_url,
"application-notification_title": "fallback-title " + default_notification_title, "application-notification_title": "fallback-title " + default_notification_title,
"application-notification_body": "fallback-body<br> " + default_notification_body, "application-notification_body": "fallback-body<br> " + default_notification_body,
@@ -64,7 +64,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas
# Add a watch and trigger a HTTP POST # Add a watch and trigger a HTTP POST
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": 'nice one'}, data={"url": test_url, "tags": 'nice one'},
follow_redirects=True follow_redirects=True
) )
@@ -75,7 +75,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas
set_longer_modified_response() set_longer_modified_response()
time.sleep(2) time.sleep(2)
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
time.sleep(3) time.sleep(3)
@@ -88,7 +88,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas
assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n
assert 'Content-Type: text/html' in msg assert 'Content-Type: text/html' in msg
assert '(added) So let\'s see what happens.<br>' in msg # the html part assert '(added) So let\'s see what happens.<br>' in msg # the html part
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
@@ -116,7 +116,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
##################### #####################
# Set this up for when we remove the notification from the watch, it should fallback with these details # Set this up for when we remove the notification from the watch, it should fallback with these details
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-notification_urls": notification_url, data={"application-notification_urls": notification_url,
"application-notification_title": "fallback-title " + default_notification_title, "application-notification_title": "fallback-title " + default_notification_title,
"application-notification_body": notification_body, "application-notification_body": notification_body,
@@ -130,7 +130,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
# Add a watch and trigger a HTTP POST # Add a watch and trigger a HTTP POST
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": 'nice one'}, data={"url": test_url, "tags": 'nice one'},
follow_redirects=True follow_redirects=True
) )
@@ -140,7 +140,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
wait_for_all_checks(client) wait_for_all_checks(client)
set_longer_modified_response() set_longer_modified_response()
time.sleep(2) time.sleep(2)
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
time.sleep(3) time.sleep(3)
@@ -157,7 +157,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
set_original_response() set_original_response()
# Now override as HTML format # Now override as HTML format
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={ data={
"url": test_url, "url": test_url,
"notification_format": 'HTML', "notification_format": 'HTML',
@@ -182,5 +182,5 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
assert '&lt;' not in msg assert '&lt;' not in msg
assert 'Content-Type: text/html' in msg assert 'Content-Type: text/html' in msg
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -1,4 +1,4 @@
from .util import live_server_setup, extract_UUID_from_client, wait_for_all_checks from .util import live_server_setup
from flask import url_for from flask import url_for
import time import time
@@ -8,12 +8,12 @@ def test_check_access_control(app, client, live_server):
with app.test_client(use_cookies=True) as c: with app.test_client(use_cookies=True) as c:
# Check we don't have any password protection enabled yet. # Check we don't have any password protection enabled yet.
res = c.get(url_for("settings_page")) res = c.get(url_for("settings.settings_page"))
assert b"Remove password" not in res.data assert b"Remove password" not in res.data
# add something that we can hit via diff page later # add something that we can hit via diff page later
res = c.post( res = c.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": url_for('test_random_content_endpoint', _external=True)}, data={"urls": url_for('test_random_content_endpoint', _external=True)},
follow_redirects=True follow_redirects=True
) )
@@ -23,8 +23,9 @@ def test_check_access_control(app, client, live_server):
# causes a 'Popped wrong request context.' error when client. is accessed? # causes a 'Popped wrong request context.' error when client. is accessed?
#wait_for_all_checks(client) #wait_for_all_checks(client)
res = c.get(url_for("form_watch_checknow"), follow_redirects=True) res = c.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
time.sleep(3) time.sleep(3)
# causes a 'Popped wrong request context.' error when client. is accessed? # causes a 'Popped wrong request context.' error when client. is accessed?
#wait_for_all_checks(client) #wait_for_all_checks(client)
@@ -32,7 +33,7 @@ def test_check_access_control(app, client, live_server):
# Enable password check and diff page access bypass # Enable password check and diff page access bypass
res = c.post( res = c.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-password": "foobar", data={"application-password": "foobar",
"application-shared_diff_access": "True", "application-shared_diff_access": "True",
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
@@ -48,7 +49,7 @@ def test_check_access_control(app, client, live_server):
assert b"Login" in res.data assert b"Login" in res.data
# The diff page should return something valid when logged out # The diff page should return something valid when logged out
res = c.get(url_for("diff_history_page", uuid="first")) res = c.get(url_for("ui.ui_views.diff_history_page", uuid="first"))
assert b'Random content' in res.data assert b'Random content' in res.data
# Check wrong password does not let us in # Check wrong password does not let us in
@@ -79,7 +80,7 @@ def test_check_access_control(app, client, live_server):
# 598 - Password should be set and not accidently removed # 598 - Password should be set and not accidently removed
res = c.post( res = c.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_requests"}, 'application-fetch_backend': "html_requests"},
@@ -91,7 +92,7 @@ def test_check_access_control(app, client, live_server):
assert b"Login" in res.data assert b"Login" in res.data
res = c.get(url_for("settings_page"), res = c.get(url_for("settings.settings_page"),
follow_redirects=True) follow_redirects=True)
@@ -110,7 +111,7 @@ def test_check_access_control(app, client, live_server):
# Yes we are correctly logged in # Yes we are correctly logged in
assert b"LOG OUT" in res.data assert b"LOG OUT" in res.data
res = c.get(url_for("settings_page")) res = c.get(url_for("settings.settings_page"))
# Menu should be available now # Menu should be available now
assert b"SETTINGS" in res.data assert b"SETTINGS" in res.data
@@ -124,7 +125,7 @@ def test_check_access_control(app, client, live_server):
# Remove password button, and check that it worked # Remove password button, and check that it worked
################################################## ##################################################
res = c.post( res = c.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-fetch_backend": "html_webdriver", "application-fetch_backend": "html_webdriver",
@@ -139,7 +140,7 @@ def test_check_access_control(app, client, live_server):
# Be sure a blank password doesnt setup password protection # Be sure a blank password doesnt setup password protection
############################################################ ############################################################
res = c.post( res = c.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-password": "", data={"application-password": "",
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_requests"}, 'application-fetch_backend': "html_requests"},
@@ -151,7 +152,7 @@ def test_check_access_control(app, client, live_server):
# Now checking the diff access # Now checking the diff access
# Enable password check and diff page access bypass # Enable password check and diff page access bypass
res = c.post( res = c.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-password": "foobar", data={"application-password": "foobar",
# Should be disabled # Should be disabled
# "application-shared_diff_access": "True", # "application-shared_diff_access": "True",
@@ -168,5 +169,5 @@ def test_check_access_control(app, client, live_server):
assert b"Login" in res.data assert b"Login" in res.data
# The diff page should return something valid when logged out # The diff page should return something valid when logged out
res = c.get(url_for("diff_history_page", uuid="first")) res = c.get(url_for("ui.ui_views.diff_history_page", uuid="first"))
assert b'Random content' not in res.data assert b'Random content' not in res.data

View File

@@ -45,7 +45,7 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -57,7 +57,7 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"trigger_text": 'The golden line', data={"trigger_text": 'The golden line',
"url": test_url, "url": test_url,
'fetch_backend': "html_requests", 'fetch_backend': "html_requests",
@@ -69,8 +69,8 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory
set_original(excluding='Something irrelevant') set_original(excluding='Something irrelevant')
# A line thats not the trigger should not trigger anything # A line thats not the trigger should not trigger anything
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' not in res.data assert b'unviewed' not in res.data
@@ -79,28 +79,28 @@ def test_check_removed_line_contains_trigger(client, live_server, measure_memory
set_original(excluding='The golden line') set_original(excluding='The golden line')
# Check in the processor here what's going on, its triggering empty-reply and no change. # Check in the processor here what's going on, its triggering empty-reply and no change.
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
# Now add it back, and we should not get a trigger # Now add it back, and we should not get a trigger
client.get(url_for("mark_all_viewed"), follow_redirects=True) client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
set_original(excluding=None) set_original(excluding=None)
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' not in res.data assert b'unviewed' not in res.data
# Remove it again, and we should get a trigger # Remove it again, and we should get a trigger
set_original(excluding='The golden line') set_original(excluding='The golden line')
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
@@ -111,7 +111,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://') + "?xxx={{ watch_url }}" test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://') + "?xxx={{ watch_url }}"
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", data={"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
# triggered_text will contain multiple lines # triggered_text will contain multiple lines
"application-notification_body": 'triggered text was -{{triggered_text}}- ### 网站监测 内容更新了 ####', "application-notification_body": 'triggered text was -{{triggered_text}}- ### 网站监测 内容更新了 ####',
@@ -128,7 +128,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -139,7 +139,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"trigger_text": 'Oh yes please', data={"trigger_text": 'Oh yes please',
"url": test_url, "url": test_url,
'processor': 'text_json_diff', 'processor': 'text_json_diff',
@@ -153,8 +153,8 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
set_original(excluding='Something irrelevant') set_original(excluding='Something irrelevant')
# A line thats not the trigger should not trigger anything # A line thats not the trigger should not trigger anything
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
@@ -162,9 +162,10 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
# The trigger line is ADDED, this should trigger # The trigger line is ADDED, this should trigger
set_original(add_line='<p>Oh yes please</p>') set_original(add_line='<p>Oh yes please</p>')
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
# Takes a moment for apprise to fire # Takes a moment for apprise to fire
@@ -175,5 +176,5 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
assert b'-Oh yes please' in response assert b'-Oh yes please' in response
assert '网站监测 内容更新了'.encode('utf-8') in response assert '网站监测 内容更新了'.encode('utf-8') in response
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -2,7 +2,7 @@
import time import time
from flask import url_for from flask import url_for
from .util import live_server_setup, extract_api_key_from_UI, wait_for_all_checks from .util import live_server_setup, wait_for_all_checks
import json import json
import uuid import uuid
@@ -57,16 +57,15 @@ def test_setup(client, live_server, measure_memory_usage):
def test_api_simple(client, live_server, measure_memory_usage): def test_api_simple(client, live_server, measure_memory_usage):
# live_server_setup(live_server) #live_server_setup(live_server)
api_key = extract_api_key_from_UI(client) api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
# Create a watch # Create a watch
set_original_response() set_original_response()
# Validate bad URL # Validate bad URL
test_url = url_for('test_endpoint', _external=True, test_url = url_for('test_endpoint', _external=True )
headers={'x-api-key': api_key}, )
res = client.post( res = client.post(
url_for("createwatch"), url_for("createwatch"),
data=json.dumps({"url": "h://xxxxxxxxxom"}), data=json.dumps({"url": "h://xxxxxxxxxom"}),
@@ -173,7 +172,7 @@ def test_api_simple(client, live_server, measure_memory_usage):
assert watch.get('viewed') == False assert watch.get('viewed') == False
# Loading the most recent snapshot should force viewed to become true # Loading the most recent snapshot should force viewed to become true
client.get(url_for("diff_history_page", uuid="first"), follow_redirects=True) client.get(url_for("ui.ui_views.diff_history_page", uuid="first"), follow_redirects=True)
time.sleep(3) time.sleep(3)
# Fetch the whole watch again, viewed should be true # Fetch the whole watch again, viewed should be true
@@ -259,7 +258,7 @@ def test_access_denied(client, live_server, measure_memory_usage):
# Disable config_api_token_enabled and it should work # Disable config_api_token_enabled and it should work
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-fetch_backend": "html_requests", "application-fetch_backend": "html_requests",
@@ -276,11 +275,11 @@ def test_access_denied(client, live_server, measure_memory_usage):
assert res.status_code == 200 assert res.status_code == 200
# Cleanup everything # Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-fetch_backend": "html_requests", "application-fetch_backend": "html_requests",
@@ -293,12 +292,11 @@ def test_access_denied(client, live_server, measure_memory_usage):
def test_api_watch_PUT_update(client, live_server, measure_memory_usage): def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
#live_server_setup(live_server) #live_server_setup(live_server)
api_key = extract_api_key_from_UI(client) api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
# Create a watch # Create a watch
set_original_response() set_original_response()
test_url = url_for('test_endpoint', _external=True, test_url = url_for('test_endpoint', _external=True)
headers={'x-api-key': api_key}, )
# Create new # Create new
res = client.post( res = client.post(
@@ -321,7 +319,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
# Check in the edit page just to be sure # Check in the edit page just to be sure
res = client.get( res = client.get(
url_for("edit_page", uuid=watch_uuid), url_for("ui.ui_edit.edit_page", uuid=watch_uuid),
) )
assert b"cookie: yum" in res.data, "'cookie: yum' found in 'headers' section" assert b"cookie: yum" in res.data, "'cookie: yum' found in 'headers' section"
assert b"One" in res.data, "Tag 'One' was found" assert b"One" in res.data, "Tag 'One' was found"
@@ -344,7 +342,7 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
# Check in the edit page just to be sure # Check in the edit page just to be sure
res = client.get( res = client.get(
url_for("edit_page", uuid=watch_uuid), url_for("ui.ui_edit.edit_page", uuid=watch_uuid),
) )
assert b"new title" in res.data, "new title found in edit page" assert b"new title" in res.data, "new title found in edit page"
assert b"552" in res.data, "552 minutes found in edit page" assert b"552" in res.data, "552 minutes found in edit page"
@@ -368,12 +366,13 @@ def test_api_watch_PUT_update(client, live_server, measure_memory_usage):
assert b'Additional properties are not allowed' in res.data assert b'Additional properties are not allowed' in res.data
# Cleanup everything # Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_api_import(client, live_server, measure_memory_usage): def test_api_import(client, live_server, measure_memory_usage):
api_key = extract_api_key_from_UI(client) #live_server_setup(live_server)
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
res = client.post( res = client.post(
url_for("import") + "?tag=import-test", url_for("import") + "?tag=import-test",
@@ -391,4 +390,48 @@ def test_api_import(client, live_server, measure_memory_usage):
# Should see the new tag in the tag/groups list # Should see the new tag in the tag/groups list
res = client.get(url_for('tags.tags_overview_page')) res = client.get(url_for('tags.tags_overview_page'))
assert b'import-test' in res.data assert b'import-test' in res.data
def test_api_conflict_UI_password(client, live_server, measure_memory_usage):
#live_server_setup(live_server)
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
# Enable password check and diff page access bypass
res = client.post(
url_for("settings.settings_page"),
data={"application-password": "foobar", # password is now set! API should still work!
"application-api_access_token_enabled": "y",
"requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_requests"},
follow_redirects=True
)
assert b"Password protection enabled." in res.data
# Create a watch
set_original_response()
test_url = url_for('test_endpoint', _external=True)
# Create new
res = client.post(
url_for("createwatch"),
data=json.dumps({"url": test_url, "title": "My test URL" }),
headers={'content-type': 'application/json', 'x-api-key': api_key},
follow_redirects=True
)
assert res.status_code == 201
wait_for_all_checks(client)
url = url_for("createwatch")
# Get a listing, it will be the first one
res = client.get(
url,
headers={'x-api-key': api_key}
)
assert res.status_code == 200
assert len(res.json)

View File

@@ -13,7 +13,7 @@ def test_basic_auth(client, live_server, measure_memory_usage):
test_url = url_for('test_basicauth_method', _external=True).replace("//","//myuser:mypass@") test_url = url_for('test_basicauth_method', _external=True).replace("//","//myuser:mypass@")
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -22,7 +22,7 @@ def test_basic_auth(client, live_server, measure_memory_usage):
time.sleep(1) time.sleep(1)
# Check form validation # Check form validation
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": "", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, data={"include_filters": "", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"},
follow_redirects=True follow_redirects=True
) )
@@ -30,7 +30,7 @@ def test_basic_auth(client, live_server, measure_memory_usage):
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )

View File

@@ -2,7 +2,7 @@
import time import time
from flask import url_for from flask import url_for
from .util import live_server_setup, extract_UUID_from_client, extract_api_key_from_UI, wait_for_all_checks from .util import live_server_setup, extract_UUID_from_client, wait_for_all_checks
def set_response_with_ldjson(): def set_response_with_ldjson():
@@ -87,7 +87,7 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -99,10 +99,10 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage
assert b'ldjson-price-track-offer' in res.data assert b'ldjson-price-track-offer' in res.data
# Accept it # Accept it
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
#time.sleep(1) #time.sleep(1)
client.get(url_for('price_data_follower.accept', uuid=uuid, follow_redirects=True)) client.get(url_for('price_data_follower.accept', uuid=uuid, follow_redirects=True))
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
# Offer should be gone # Offer should be gone
res = client.get(url_for("index")) res = client.get(url_for("index"))
@@ -110,7 +110,7 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage
assert b'tracking-ldjson-price-data' in res.data assert b'tracking-ldjson-price-data' in res.data
# and last snapshop (via API) should be just the price # and last snapshop (via API) should be just the price
api_key = extract_api_key_from_UI(client) api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
res = client.get( res = client.get(
url_for("watchsinglehistory", uuid=uuid, timestamp='latest'), url_for("watchsinglehistory", uuid=uuid, timestamp='latest'),
headers={'x-api-key': api_key}, headers={'x-api-key': api_key},
@@ -121,7 +121,7 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage
# And not this cause its not the ld-json # And not this cause its not the ld-json
assert b"So let's see what happens" not in res.data assert b"So let's see what happens" not in res.data
client.get(url_for("form_delete", uuid="all"), follow_redirects=True) client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
########################################################################################## ##########################################################################################
# And we shouldnt see the offer # And we shouldnt see the offer
@@ -130,7 +130,7 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -140,14 +140,14 @@ def test_check_ldjson_price_autodetect(client, live_server, measure_memory_usage
assert b'ldjson-price-track-offer' not in res.data assert b'ldjson-price-track-offer' not in res.data
########################################################################################## ##########################################################################################
client.get(url_for("form_delete", uuid="all"), follow_redirects=True) client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
def _test_runner_check_bad_format_ignored(live_server, client, has_ldjson_price_data): def _test_runner_check_bad_format_ignored(live_server, client, has_ldjson_price_data):
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -160,7 +160,7 @@ def _test_runner_check_bad_format_ignored(live_server, client, has_ldjson_price_
########################################################################################## ##########################################################################################
client.get(url_for("form_delete", uuid="all"), follow_redirects=True) client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
def test_bad_ldjson_is_correctly_ignored(client, live_server, measure_memory_usage): def test_bad_ldjson_is_correctly_ignored(client, live_server, measure_memory_usage):

View File

@@ -22,7 +22,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": url_for('test_endpoint', _external=True)}, data={"urls": url_for('test_endpoint', _external=True)},
follow_redirects=True follow_redirects=True
) )
@@ -33,7 +33,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
# Do this a few times.. ensures we dont accidently set the status # Do this a few times.. ensures we dont accidently set the status
for n in range(3): for n in range(3):
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -53,7 +53,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
# Check HTML conversion detected and workd # Check HTML conversion detected and workd
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
# Check this class does not appear (that we didnt see the actual source) # Check this class does not appear (that we didnt see the actual source)
@@ -63,15 +63,15 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
set_modified_response() set_modified_response()
# Force recheck # Force recheck
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
# Check the 'get latest snapshot works' # Check the 'get latest snapshot works'
res = client.get(url_for("watch_get_latest_html", uuid=uuid)) res = client.get(url_for("ui.ui_edit.watch_get_latest_html", uuid=uuid))
assert b'which has this one new line' in res.data assert b'which has this one new line' in res.data
# Now something should be ready, indicated by having a 'unviewed' class # Now something should be ready, indicated by having a 'unviewed' class
@@ -80,7 +80,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
# #75, and it should be in the RSS feed # #75, and it should be in the RSS feed
rss_token = extract_rss_token_from_UI(client) rss_token = extract_rss_token_from_UI(client)
res = client.get(url_for("rss", token=rss_token, _external=True)) res = client.get(url_for("rss.feed", token=rss_token, _external=True))
expected_url = url_for('test_endpoint', _external=True) expected_url = url_for('test_endpoint', _external=True)
assert b'<rss' in res.data assert b'<rss' in res.data
@@ -91,12 +91,12 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
assert expected_url.encode('utf-8') in res.data assert expected_url.encode('utf-8') in res.data
# Following the 'diff' link, it should no longer display as 'unviewed' even after we recheck it a few times # Following the 'diff' link, it should no longer display as 'unviewed' even after we recheck it a few times
res = client.get(url_for("diff_history_page", uuid=uuid)) res = client.get(url_for("ui.ui_views.diff_history_page", uuid=uuid))
assert b'selected=""' in res.data, "Confirm diff history page loaded" assert b'selected=""' in res.data, "Confirm diff history page loaded"
# Check the [preview] pulls the right one # Check the [preview] pulls the right one
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
assert b'which has this one new line' in res.data assert b'which has this one new line' in res.data
@@ -106,7 +106,7 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
# Do this a few times.. ensures we dont accidently set the status # Do this a few times.. ensures we dont accidently set the status
for n in range(2): for n in range(2):
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -122,13 +122,13 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
# Enable auto pickup of <title> in settings # Enable auto pickup of <title> in settings
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-extract_title_as_title": "1", "requests-time_between_check-minutes": 180, data={"application-extract_title_as_title": "1", "requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_requests"}, 'application-fetch_backend': "html_requests"},
follow_redirects=True follow_redirects=True
) )
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
@@ -142,19 +142,19 @@ def test_check_basic_change_detection_functionality(client, live_server, measure
time.sleep(1) time.sleep(1)
# hit the mark all viewed link # hit the mark all viewed link
res = client.get(url_for("mark_all_viewed"), follow_redirects=True) res = client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
assert b'Mark all viewed' not in res.data assert b'Mark all viewed' not in res.data
assert b'unviewed' not in res.data assert b'unviewed' not in res.data
# #2458 "clear history" should make the Watch object update its status correctly when the first snapshot lands again # #2458 "clear history" should make the Watch object update its status correctly when the first snapshot lands again
client.get(url_for("clear_watch_history", uuid=uuid)) client.get(url_for("ui.clear_watch_history", uuid=uuid))
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'preview/' in res.data assert b'preview/' in res.data
# #
# Cleanup everything # Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -18,7 +18,7 @@ def test_backup(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": url_for('test_endpoint', _external=True)+"?somechar=őőőőőőőő"}, data={"urls": url_for('test_endpoint', _external=True)+"?somechar=őőőőőőőő"},
follow_redirects=True follow_redirects=True
) )

View File

@@ -71,7 +71,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -83,7 +83,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"text_should_not_be_present": ignore_text, data={"text_should_not_be_present": ignore_text,
"url": test_url, "url": test_url,
'fetch_backend': "html_requests" 'fetch_backend': "html_requests"
@@ -96,12 +96,12 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
wait_for_all_checks(client) wait_for_all_checks(client)
# Check it saved # Check it saved
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
) )
assert bytes(ignore_text.encode('utf-8')) in res.data assert bytes(ignore_text.encode('utf-8')) in res.data
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -115,7 +115,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
set_modified_original_ignore_response() set_modified_original_ignore_response()
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -127,7 +127,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
# 2548 # 2548
# Going back to the ORIGINAL should NOT trigger a change # Going back to the ORIGINAL should NOT trigger a change
set_original_ignore_response() set_original_ignore_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' not in res.data assert b'unviewed' not in res.data
@@ -135,7 +135,7 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
# Now we set a change where the text is gone AND its different content, it should now trigger # Now we set a change where the text is gone AND its different content, it should now trigger
set_modified_response_minus_block_text() set_modified_response_minus_block_text()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
@@ -143,5 +143,5 @@ def test_check_block_changedetection_text_NOT_present(client, live_server, measu
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -15,7 +15,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": "https://changedetection.io"}, data={"urls": "https://changedetection.io"},
follow_redirects=True follow_redirects=True
) )
@@ -23,7 +23,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
res = client.get( res = client.get(
url_for("form_clone", uuid="first"), url_for("ui.form_clone", uuid="first"),
follow_redirects=True follow_redirects=True
) )

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
import json
import urllib
from flask import url_for
from .util import live_server_setup, wait_for_all_checks
def set_original_response(number="50"):
test_return_data = f"""<html>
<body>
<h1>Test Page for Conditions</h1>
<p>This page contains a number that will be tested with conditions.</p>
<div class="number-container">Current value: {number}</div>
</body>
</html>
"""
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write(test_return_data)
def set_number_in_range_response(number="75"):
test_return_data = f"""<html>
<body>
<h1>Test Page for Conditions</h1>
<p>This page contains a number that will be tested with conditions.</p>
<div class="number-container">Current value: {number}</div>
</body>
</html>
"""
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write(test_return_data)
def set_number_out_of_range_response(number="150"):
test_return_data = f"""<html>
<body>
<h1>Test Page for Conditions</h1>
<p>This page contains a number that will be tested with conditions.</p>
<div class="number-container">Current value: {number}</div>
</body>
</html>
"""
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write(test_return_data)
def test_conditions_with_text_and_number(client, live_server):
"""Test that both text and number conditions work together with AND logic."""
set_original_response("50")
live_server_setup(live_server)
test_url = url_for('test_endpoint', _external=True)
# Add our URL to the import page
res = client.post(
url_for("imports.import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
wait_for_all_checks(client)
# Configure the watch with two conditions connected with AND:
# 1. The page filtered text must contain "5" (first digit of value)
# 2. The extracted number should be >= 20 and <= 100
res = client.post(
url_for("ui.ui_edit.edit_page", uuid="first"),
data={
"url": test_url,
"fetch_backend": "html_requests",
"include_filters": ".number-container",
"title": "Number AND Text Condition Test",
"conditions_match_logic": "ALL", # ALL = AND logic
"conditions-0-operator": "in",
"conditions-0-field": "page_filtered_text",
"conditions-0-value": "5",
"conditions-1-operator": ">=",
"conditions-1-field": "extracted_number",
"conditions-1-value": "20",
"conditions-2-operator": "<=",
"conditions-2-field": "extracted_number",
"conditions-2-value": "100",
# So that 'operations' from pluggy discovery are tested
"conditions-3-operator": "length_min",
"conditions-3-field": "page_filtered_text",
"conditions-3-value": "1",
# So that 'operations' from pluggy discovery are tested
"conditions-4-operator": "length_max",
"conditions-4-field": "page_filtered_text",
"conditions-4-value": "100",
# So that 'operations' from pluggy discovery are tested
"conditions-5-operator": "contains_regex",
"conditions-5-field": "page_filtered_text",
"conditions-5-value": "\d",
},
follow_redirects=True
)
assert b"Updated watch." in res.data
wait_for_all_checks(client)
client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
wait_for_all_checks(client)
# Case 1
set_number_in_range_response("70.5")
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client)
# 75 is > 20 and < 100 and contains "5"
res = client.get(url_for("index"))
assert b'unviewed' in res.data
# Case 2: Change with one condition violated
# Number out of range (150) but contains '5'
client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
set_number_out_of_range_response("150.5")
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client)
# Should NOT be marked as having changes since not all conditions are met
res = client.get(url_for("index"))
assert b'unviewed' not in res.data
res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data
# The 'validate' button next to each rule row
def test_condition_validate_rule_row(client, live_server):
set_original_response("50")
test_url = url_for('test_endpoint', _external=True)
# Add our URL to the import page
res = client.post(
url_for("imports.import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
wait_for_all_checks(client)
uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
# the front end submits the current form state which should override the watch in a temporary copy
res = client.post(
url_for("conditions.verify_condition_single_rule", watch_uuid=uuid), # Base URL
query_string={"rule": json.dumps({"field": "extracted_number", "operator": "==", "value": "50"})},
data={'include_filter': ""},
follow_redirects=True
)
assert res.status_code == 200
assert b'success' in res.data
# Now a number that does not equal what is found in the last fetch
res = client.post(
url_for("conditions.verify_condition_single_rule", watch_uuid=uuid), # Base URL
query_string={"rule": json.dumps({"field": "extracted_number", "operator": "==", "value": "111111"})},
data={'include_filter': ""},
follow_redirects=True
)
assert res.status_code == 200
assert b'false' in res.data
# Now custom filter that exists
res = client.post(
url_for("conditions.verify_condition_single_rule", watch_uuid=uuid), # Base URL
query_string={"rule": json.dumps({"field": "extracted_number", "operator": "==", "value": "50"})},
data={'include_filter': ".number-container"},
follow_redirects=True
)
assert res.status_code == 200
assert b'success' in res.data
# Now custom filter that DOES NOT exists
res = client.post(
url_for("conditions.verify_condition_single_rule", watch_uuid=uuid), # Base URL
query_string={"rule": json.dumps({"field": "extracted_number", "operator": "==", "value": "50"})},
data={'include_filters': ".NOT-container"},
follow_redirects=True
)
assert res.status_code == 200
assert b'false' in res.data

View File

@@ -83,7 +83,7 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -95,7 +95,7 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": include_filters, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, data={"include_filters": include_filters, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"},
follow_redirects=True follow_redirects=True
) )
@@ -103,7 +103,7 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m
time.sleep(1) time.sleep(1)
# Check it saved # Check it saved
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
) )
assert bytes(include_filters.encode('utf-8')) in res.data assert bytes(include_filters.encode('utf-8')) in res.data
@@ -113,7 +113,7 @@ def test_check_markup_include_filters_restriction(client, live_server, measure_m
set_modified_response() set_modified_response()
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
@@ -140,7 +140,7 @@ def test_check_multiple_filters(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -150,7 +150,7 @@ def test_check_multiple_filters(client, live_server, measure_memory_usage):
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": include_filters, data={"include_filters": include_filters,
"url": test_url, "url": test_url,
"tags": "", "tags": "",
@@ -164,7 +164,7 @@ def test_check_multiple_filters(client, live_server, measure_memory_usage):
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
@@ -194,7 +194,7 @@ def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usa
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -204,7 +204,7 @@ def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usa
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": include_filters, data={"include_filters": include_filters,
"url": test_url, "url": test_url,
"tags": "", "tags": "",
@@ -236,7 +236,7 @@ def test_filter_is_empty_help_suggestion(client, live_server, measure_memory_usa
</html> </html>
""") """)
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(

View File

@@ -156,7 +156,7 @@ def test_element_removal_full(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
test_url = url_for("test_endpoint", _external=True) test_url = url_for("test_endpoint", _external=True)
res = client.post( res = client.post(
url_for("import_page"), data={"urls": test_url}, follow_redirects=True url_for("imports.import_page"), data={"urls": test_url}, follow_redirects=True
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -165,7 +165,7 @@ def test_element_removal_full(client, live_server, measure_memory_usage):
# Not sure why \r needs to be added - absent of the #changetext this is not necessary # Not sure why \r needs to be added - absent of the #changetext this is not necessary
subtractive_selectors_data = "header\r\nfooter\r\nnav\r\n#changetext" subtractive_selectors_data = "header\r\nfooter\r\nnav\r\n#changetext"
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={ data={
"subtractive_selectors": subtractive_selectors_data, "subtractive_selectors": subtractive_selectors_data,
"url": test_url, "url": test_url,
@@ -180,25 +180,25 @@ def test_element_removal_full(client, live_server, measure_memory_usage):
# Check it saved # Check it saved
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
) )
assert bytes(subtractive_selectors_data.encode("utf-8")) in res.data assert bytes(subtractive_selectors_data.encode("utf-8")) in res.data
# Trigger a check # Trigger a check
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
# so that we set the state to 'unviewed' after all the edits # so that we set the state to 'unviewed' after all the edits
client.get(url_for("diff_history_page", uuid="first")) client.get(url_for("ui.ui_views.diff_history_page", uuid="first"))
# Make a change to header/footer/nav # Make a change to header/footer/nav
set_modified_response() set_modified_response()
# Trigger a check # Trigger a check
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'Queued 1 watch for rechecking.' in res.data
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -228,19 +228,19 @@ body > table > tr:nth-child(3) > td:nth-child(3)""",
for selector_list in subtractive_selectors_data: for selector_list in subtractive_selectors_data:
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
# Add our URL to the import page # Add our URL to the import page
test_url = url_for("test_endpoint", _external=True) test_url = url_for("test_endpoint", _external=True)
res = client.post( res = client.post(
url_for("import_page"), data={"urls": test_url}, follow_redirects=True url_for("imports.import_page"), data={"urls": test_url}, follow_redirects=True
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={ data={
"subtractive_selectors": selector_list, "subtractive_selectors": selector_list,
"url": test_url, "url": test_url,
@@ -253,7 +253,7 @@ body > table > tr:nth-child(3) > td:nth-child(3)""",
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )

View File

@@ -30,7 +30,7 @@ def test_check_encoding_detection(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', content_type="text/html", _external=True) test_url = url_for('test_endpoint', content_type="text/html", _external=True)
client.post( client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -40,11 +40,11 @@ def test_check_encoding_detection(client, live_server, measure_memory_usage):
# Content type recording worked # Content type recording worked
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['content-type'] == "text/html" assert live_server.app.config['DATASTORE'].data['watching'][uuid]['content-type'] == "text/html"
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
@@ -61,7 +61,7 @@ def test_check_encoding_detection_missing_content_type_header(client, live_serve
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
client.post( client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -69,7 +69,7 @@ def test_check_encoding_detection_missing_content_type_header(client, live_serve
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )

View File

@@ -23,7 +23,7 @@ def _runner_test_http_errors(client, live_server, http_code, expected_text):
_external=True) _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -40,7 +40,7 @@ def _runner_test_http_errors(client, live_server, http_code, expected_text):
# Error viewing tabs should appear # Error viewing tabs should appear
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
@@ -50,7 +50,7 @@ def _runner_test_http_errors(client, live_server, http_code, expected_text):
#assert b'Error Screenshot' in res.data #assert b'Error Screenshot' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
@@ -59,7 +59,7 @@ def test_http_error_handler(client, live_server, measure_memory_usage):
_runner_test_http_errors(client, live_server, 404, 'Page not found') _runner_test_http_errors(client, live_server, 404, 'Page not found')
_runner_test_http_errors(client, live_server, 500, '(Internal server error) received') _runner_test_http_errors(client, live_server, 500, '(Internal server error) received')
_runner_test_http_errors(client, live_server, 400, 'Error - Request returned a HTTP error code 400') _runner_test_http_errors(client, live_server, 400, 'Error - Request returned a HTTP error code 400')
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
# Just to be sure error text is properly handled # Just to be sure error text is properly handled
@@ -69,7 +69,7 @@ def test_DNS_errors(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": "https://errorfuldomainthatnevereallyexists12356.com"}, data={"urls": "https://errorfuldomainthatnevereallyexists12356.com"},
follow_redirects=True follow_redirects=True
) )
@@ -83,7 +83,7 @@ def test_DNS_errors(client, live_server, measure_memory_usage):
assert found_name_resolution_error assert found_name_resolution_error
# Should always record that we tried # Should always record that we tried
assert bytes("just now".encode('utf-8')) in res.data assert bytes("just now".encode('utf-8')) in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
# Re 1513 # Re 1513
@@ -99,7 +99,7 @@ def test_low_level_errors_clear_correctly(client, live_server, measure_memory_us
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": "https://dfkjasdkfjaidjfsdajfksdajfksdjfDOESNTEXIST.com"}, data={"urls": "https://dfkjasdkfjaidjfsdajfksdajfksdjfDOESNTEXIST.com"},
follow_redirects=True follow_redirects=True
) )
@@ -113,7 +113,7 @@ def test_low_level_errors_clear_correctly(client, live_server, measure_memory_us
# Update with what should work # Update with what should work
client.post( client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={ data={
"url": test_url, "url": test_url,
"fetch_backend": "html_requests"}, "fetch_backend": "html_requests"},
@@ -126,5 +126,5 @@ def test_low_level_errors_clear_correctly(client, live_server, measure_memory_us
found_name_resolution_error = b"Temporary failure in name resolution" in res.data or b"Name or service not known" in res.data found_name_resolution_error = b"Temporary failure in name resolution" in res.data or b"Name or service not known" in res.data
assert not found_name_resolution_error assert not found_name_resolution_error
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -18,7 +18,7 @@ def test_check_extract_text_from_diff(client, live_server, measure_memory_usage)
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": url_for('test_endpoint', _external=True)}, data={"urls": url_for('test_endpoint', _external=True)},
follow_redirects=True follow_redirects=True
) )
@@ -36,11 +36,11 @@ def test_check_extract_text_from_diff(client, live_server, measure_memory_usage)
with open("test-datastore/endpoint-content.txt", "w") as f: with open("test-datastore/endpoint-content.txt", "w") as f:
f.write("Now it's {} seconds since epoch, time flies!".format(last_date)) f.write("Now it's {} seconds since epoch, time flies!".format(last_date))
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.post( res = client.post(
url_for("diff_history_page", uuid="first"), url_for("ui.ui_views.diff_history_page", uuid="first"),
data={"extract_regex": "Now it's ([0-9\.]+)", data={"extract_regex": "Now it's ([0-9\.]+)",
"extract_submit_button": "Extract as CSV"}, "extract_submit_button": "Extract as CSV"},
follow_redirects=False follow_redirects=False

View File

@@ -77,7 +77,7 @@ def test_check_filter_multiline(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -88,7 +88,7 @@ def test_check_filter_multiline(client, live_server, measure_memory_usage):
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": '', data={"include_filters": '',
# Test a regex and a plaintext # Test a regex and a plaintext
'extract_text': '/something.+?6 billion.+?lines/si\r\nand this should be', 'extract_text': '/something.+?6 billion.+?lines/si\r\nand this should be',
@@ -109,7 +109,7 @@ def test_check_filter_multiline(client, live_server, measure_memory_usage):
assert b'not at the start of the expression' not in res.data assert b'not at the start of the expression' not in res.data
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
# Plaintext that doesnt look like a regex should match also # Plaintext that doesnt look like a regex should match also
@@ -131,7 +131,7 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -143,7 +143,7 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": include_filters, data={"include_filters": include_filters,
'extract_text': '/\d+ online/\r\n/\d+ guests/\r\n/somecase insensitive \d+/i\r\n/somecase insensitive (345\d)/i\r\n/issue1828.+?2022/i', 'extract_text': '/\d+ online/\r\n/\d+ guests/\r\n/somecase insensitive \d+/i\r\n/somecase insensitive (345\d)/i\r\n/issue1828.+?2022/i',
"url": test_url, "url": test_url,
@@ -168,7 +168,7 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag
set_modified_response() set_modified_response()
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -179,7 +179,7 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag
# Check HTML conversion detected and workd # Check HTML conversion detected and workd
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
@@ -211,7 +211,7 @@ def test_regex_error_handling(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -219,7 +219,7 @@ def test_regex_error_handling(client, live_server, measure_memory_usage):
### test regex error handling ### test regex error handling
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"extract_text": '/something bad\d{3/XYZ', data={"extract_text": '/something bad\d{3/XYZ',
"url": test_url, "url": test_url,
"fetch_backend": "html_requests"}, "fetch_backend": "html_requests"},
@@ -228,5 +228,5 @@ def test_regex_error_handling(client, live_server, measure_memory_usage):
assert b'is not a valid regular expression.' in res.data assert b'is not a valid regular expression.' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -55,7 +55,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": 'cinema'}, data={"url": test_url, "tags": 'cinema'},
follow_redirects=True follow_redirects=True
) )
@@ -97,7 +97,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
"fetch_backend": "html_requests"}) "fetch_backend": "html_requests"})
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data=notification_form_data, data=notification_form_data,
follow_redirects=True follow_redirects=True
) )
@@ -108,7 +108,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
assert not os.path.isfile("test-datastore/notification.txt") assert not os.path.isfile("test-datastore/notification.txt")
# Now the filter should exist # Now the filter should exist
set_response_with_filter() set_response_with_filter()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_notification_endpoint_output() wait_for_notification_endpoint_output()

View File

@@ -36,14 +36,14 @@ def run_filter_test(client, live_server, content_filter):
# cleanup for the next # cleanup for the next
client.get( client.get(
url_for("form_delete", uuid="all"), url_for("ui.form_delete", uuid="all"),
follow_redirects=True follow_redirects=True
) )
if os.path.isfile("test-datastore/notification.txt"): if os.path.isfile("test-datastore/notification.txt"):
os.unlink("test-datastore/notification.txt") os.unlink("test-datastore/notification.txt")
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -51,7 +51,7 @@ def run_filter_test(client, live_server, content_filter):
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['consecutive_filter_failures'] == 0, "No filter = No filter failure" assert live_server.app.config['DATASTORE'].data['watching'][uuid]['consecutive_filter_failures'] == 0, "No filter = No filter failure"
@@ -80,7 +80,7 @@ def run_filter_test(client, live_server, content_filter):
} }
res = client.post( res = client.post(
url_for("edit_page", uuid=uuid), url_for("ui.ui_edit.edit_page", uuid=uuid),
data=watch_data, data=watch_data,
follow_redirects=True follow_redirects=True
) )
@@ -91,7 +91,7 @@ def run_filter_test(client, live_server, content_filter):
# Now add a filter, because recheck hours == 5, ONLY pressing of the [edit] or [recheck all] should trigger # Now add a filter, because recheck hours == 5, ONLY pressing of the [edit] or [recheck all] should trigger
watch_data['include_filters'] = content_filter watch_data['include_filters'] = content_filter
res = client.post( res = client.post(
url_for("edit_page", uuid=uuid), url_for("ui.ui_edit.edit_page", uuid=uuid),
data=watch_data, data=watch_data,
follow_redirects=True follow_redirects=True
) )
@@ -111,7 +111,7 @@ def run_filter_test(client, live_server, content_filter):
ATTEMPT_THRESHOLD_SETTING = live_server.app.config['DATASTORE'].data['settings']['application'].get('filter_failure_notification_threshold_attempts', 0) ATTEMPT_THRESHOLD_SETTING = live_server.app.config['DATASTORE'].data['settings']['application'].get('filter_failure_notification_threshold_attempts', 0)
for i in range(0, ATTEMPT_THRESHOLD_SETTING - 2): for i in range(0, ATTEMPT_THRESHOLD_SETTING - 2):
checked += 1 checked += 1
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'Warning, no filters were found' in res.data assert b'Warning, no filters were found' in res.data
@@ -122,7 +122,7 @@ def run_filter_test(client, live_server, content_filter):
time.sleep(2) time.sleep(2)
# One more check should trigger the _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT threshold # One more check should trigger the _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT threshold
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
wait_for_notification_endpoint_output() wait_for_notification_endpoint_output()
@@ -141,7 +141,7 @@ def run_filter_test(client, live_server, content_filter):
# Try several times, it should NOT have 'filter not found' # Try several times, it should NOT have 'filter not found'
for i in range(0, ATTEMPT_THRESHOLD_SETTING + 2): for i in range(0, ATTEMPT_THRESHOLD_SETTING + 2):
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
wait_for_notification_endpoint_output() wait_for_notification_endpoint_output()
@@ -157,7 +157,7 @@ def run_filter_test(client, live_server, content_filter):
# cleanup for the next # cleanup for the next
client.get( client.get(
url_for("form_delete", uuid="all"), url_for("ui.form_delete", uuid="all"),
follow_redirects=True follow_redirects=True
) )
os.unlink("test-datastore/notification.txt") os.unlink("test-datastore/notification.txt")

View File

@@ -71,7 +71,7 @@ def test_setup_group_tag(client, live_server, measure_memory_usage):
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url + "?first-imported=1 test-tag, extra-import-tag"}, data={"urls": test_url + "?first-imported=1 test-tag, extra-import-tag"},
follow_redirects=True follow_redirects=True
) )
@@ -94,14 +94,14 @@ def test_setup_group_tag(client, live_server, measure_memory_usage):
assert b'Warning, no filters were found' not in res.data assert b'Warning, no filters were found' not in res.data
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
assert b'Should be only this' in res.data assert b'Should be only this' in res.data
assert b'And never this' not in res.data assert b'And never this' not in res.data
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
# 2307 the UI notice should appear in the placeholder # 2307 the UI notice should appear in the placeholder
@@ -110,24 +110,24 @@ def test_setup_group_tag(client, live_server, measure_memory_usage):
# RSS Group tag filter # RSS Group tag filter
# An extra one that should be excluded # An extra one that should be excluded
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url + "?should-be-excluded=1 some-tag"}, data={"urls": test_url + "?should-be-excluded=1 some-tag"},
follow_redirects=True follow_redirects=True
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
set_modified_response() set_modified_response()
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
rss_token = extract_rss_token_from_UI(client) rss_token = extract_rss_token_from_UI(client)
res = client.get( res = client.get(
url_for("rss", token=rss_token, tag="extra-import-tag", _external=True), url_for("rss.feed", token=rss_token, tag="extra-import-tag", _external=True),
follow_redirects=True follow_redirects=True
) )
assert b"should-be-excluded" not in res.data assert b"should-be-excluded" not in res.data
assert res.status_code == 200 assert res.status_code == 200
assert b"first-imported=1" in res.data assert b"first-imported=1" in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_tag_import_singular(client, live_server, measure_memory_usage): def test_tag_import_singular(client, live_server, measure_memory_usage):
@@ -135,7 +135,7 @@ def test_tag_import_singular(client, live_server, measure_memory_usage):
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url + " test-tag, test-tag\r\n"+ test_url + "?x=1 test-tag, test-tag\r\n"}, data={"urls": test_url + " test-tag, test-tag\r\n"+ test_url + "?x=1 test-tag, test-tag\r\n"},
follow_redirects=True follow_redirects=True
) )
@@ -147,7 +147,7 @@ def test_tag_import_singular(client, live_server, measure_memory_usage):
) )
# Should be only 1 tag because they both had the same # Should be only 1 tag because they both had the same
assert res.data.count(b'test-tag') == 1 assert res.data.count(b'test-tag') == 1
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_tag_add_in_ui(client, live_server, measure_memory_usage): def test_tag_add_in_ui(client, live_server, measure_memory_usage):
@@ -164,7 +164,7 @@ def test_tag_add_in_ui(client, live_server, measure_memory_usage):
res = client.get(url_for("tags.delete_all"), follow_redirects=True) res = client.get(url_for("tags.delete_all"), follow_redirects=True)
assert b'All tags deleted' in res.data assert b'All tags deleted' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_group_tag_notification(client, live_server, measure_memory_usage): def test_group_tag_notification(client, live_server, measure_memory_usage):
@@ -173,7 +173,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage):
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": 'test-tag, other-tag'}, data={"url": test_url, "tags": 'test-tag, other-tag'},
follow_redirects=True follow_redirects=True
) )
@@ -211,7 +211,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage):
wait_for_all_checks(client) wait_for_all_checks(client)
set_modified_response() set_modified_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
time.sleep(3) time.sleep(3)
assert os.path.isfile("test-datastore/notification.txt") assert os.path.isfile("test-datastore/notification.txt")
@@ -232,7 +232,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage):
#@todo Test that multiple notifications fired #@todo Test that multiple notifications fired
#@todo Test that each of multiple notifications with different settings #@todo Test that each of multiple notifications with different settings
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_limit_tag_ui(client, live_server, measure_memory_usage): def test_limit_tag_ui(client, live_server, measure_memory_usage):
@@ -248,7 +248,7 @@ def test_limit_tag_ui(client, live_server, measure_memory_usage):
urls.append(test_url+"?non-grouped="+str(i)) urls.append(test_url+"?non-grouped="+str(i))
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": "\r\n".join(urls)}, data={"urls": "\r\n".join(urls)},
follow_redirects=True follow_redirects=True
) )
@@ -269,7 +269,7 @@ def test_limit_tag_ui(client, live_server, measure_memory_usage):
assert b'test-tag' in res.data assert b'test-tag' in res.data
assert res.data.count(b'processor-text_json_diff') == 20 assert res.data.count(b'processor-text_json_diff') == 20
assert b"object at" not in res.data assert b"object at" not in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
res = client.get(url_for("tags.delete_all"), follow_redirects=True) res = client.get(url_for("tags.delete_all"), follow_redirects=True)
assert b'All tags deleted' in res.data assert b'All tags deleted' in res.data
@@ -277,7 +277,7 @@ def test_clone_tag_on_import(client, live_server, measure_memory_usage):
#live_server_setup(live_server) #live_server_setup(live_server)
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url + " test-tag, another-tag\r\n"}, data={"urls": test_url + " test-tag, another-tag\r\n"},
follow_redirects=True follow_redirects=True
) )
@@ -288,14 +288,14 @@ def test_clone_tag_on_import(client, live_server, measure_memory_usage):
assert b'test-tag' in res.data assert b'test-tag' in res.data
assert b'another-tag' in res.data assert b'another-tag' in res.data
watch_uuid = extract_UUID_from_client(client) watch_uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
res = client.get(url_for("form_clone", uuid=watch_uuid), follow_redirects=True) res = client.get(url_for("ui.form_clone", uuid=watch_uuid), follow_redirects=True)
assert b'Cloned' in res.data assert b'Cloned' in res.data
# 2 times plus the top link to tag # 2 times plus the top link to tag
assert res.data.count(b'test-tag') == 3 assert res.data.count(b'test-tag') == 3
assert res.data.count(b'another-tag') == 3 assert res.data.count(b'another-tag') == 3
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usage): def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usage):
@@ -304,7 +304,7 @@ def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usa
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "tags": ' test-tag, another-tag '}, data={"url": test_url, "tags": ' test-tag, another-tag '},
follow_redirects=True follow_redirects=True
) )
@@ -315,14 +315,14 @@ def test_clone_tag_on_quickwatchform_add(client, live_server, measure_memory_usa
assert b'test-tag' in res.data assert b'test-tag' in res.data
assert b'another-tag' in res.data assert b'another-tag' in res.data
watch_uuid = extract_UUID_from_client(client) watch_uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
res = client.get(url_for("form_clone", uuid=watch_uuid), follow_redirects=True) res = client.get(url_for("ui.form_clone", uuid=watch_uuid), follow_redirects=True)
assert b'Cloned' in res.data assert b'Cloned' in res.data
# 2 times plus the top link to tag # 2 times plus the top link to tag
assert res.data.count(b'test-tag') == 3 assert res.data.count(b'test-tag') == 3
assert res.data.count(b'another-tag') == 3 assert res.data.count(b'another-tag') == 3
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
res = client.get(url_for("tags.delete_all"), follow_redirects=True) res = client.get(url_for("tags.delete_all"), follow_redirects=True)
@@ -387,7 +387,7 @@ def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measu
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -414,7 +414,7 @@ def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measu
] ]
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"include_filters": '\n'.join(filters), data={"include_filters": '\n'.join(filters),
"url": test_url, "url": test_url,
"tags": "test-tag-keep-order", "tags": "test-tag-keep-order",
@@ -426,7 +426,7 @@ def test_order_of_filters_tag_filter_and_watch_filter(client, live_server, measu
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
@@ -476,5 +476,5 @@ the {test} appeared before. {test in res.data[:n]=}
""" """
n += t_index + len(test) n += t_index + len(test)
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -16,7 +16,7 @@ def test_consistent_history(client, live_server, measure_memory_usage):
for one in r: for one in r:
test_url = url_for('test_endpoint', content_type="text/html", content=str(one), _external=True) test_url = url_for('test_endpoint', content_type="text/html", content=str(one), _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -27,7 +27,7 @@ def test_consistent_history(client, live_server, measure_memory_usage):
# Essentially just triggers the DB write/update # Essentially just triggers the DB write/update
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={"application-empty_pages_are_a_change": "", data={"application-empty_pages_are_a_change": "",
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
'application-fetch_backend': "html_requests"}, 'application-fetch_backend': "html_requests"},

View File

@@ -28,7 +28,7 @@ def test_ignore(client, live_server, measure_memory_usage):
set_original_ignore_response() set_original_ignore_response()
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -36,15 +36,15 @@ def test_ignore(client, live_server, measure_memory_usage):
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
uuid = extract_UUID_from_client(client) uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
# use the highlighter endpoint # use the highlighter endpoint
res = client.post( res = client.post(
url_for("highlight_submit_ignore_url", uuid=uuid), url_for("ui.ui_edit.highlight_submit_ignore_url", uuid=uuid),
data={"mode": 'digit-regex', 'selection': 'oh yeah 123'}, data={"mode": 'digit-regex', 'selection': 'oh yeah 123'},
follow_redirects=True follow_redirects=True
) )
res = client.get(url_for("edit_page", uuid=uuid)) res = client.get(url_for("ui.ui_edit.edit_page", uuid=uuid))
# should be a regex now # should be a regex now
assert b'/oh\ yeah\ \d+/' in res.data assert b'/oh\ yeah\ \d+/' in res.data
@@ -52,7 +52,7 @@ def test_ignore(client, live_server, measure_memory_usage):
assert b'href' in res.data assert b'href' in res.data
# It should not be in the preview anymore # It should not be in the preview anymore
res = client.get(url_for("preview_page", uuid=uuid)) res = client.get(url_for("ui.ui_views.preview_page", uuid=uuid))
assert b'<div class="ignored">oh yeah 456' not in res.data assert b'<div class="ignored">oh yeah 456' not in res.data
# Should be in base.html # Should be in base.html

View File

@@ -96,7 +96,7 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -108,7 +108,7 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa
# Goto the edit page, add our ignore text # Goto the edit page, add our ignore text
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"ignore_text": ignore_text, "url": test_url, 'fetch_backend': "html_requests"}, data={"ignore_text": ignore_text, "url": test_url, 'fetch_backend': "html_requests"},
follow_redirects=True follow_redirects=True
) )
@@ -116,12 +116,12 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa
# Check it saved # Check it saved
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
) )
assert bytes(ignore_text.encode('utf-8')) in res.data assert bytes(ignore_text.encode('utf-8')) in res.data
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -135,7 +135,7 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa
set_modified_ignore_response() set_modified_ignore_response()
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -148,20 +148,20 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa
# Just to be sure.. set a regular modified change.. # Just to be sure.. set a regular modified change..
set_modified_original_ignore_response() set_modified_original_ignore_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
res = client.get(url_for("preview_page", uuid="first")) res = client.get(url_for("ui.ui_views.preview_page", uuid="first"))
# SHOULD BE be in the preview, it was added in set_modified_original_ignore_response() # SHOULD BE be in the preview, it was added in set_modified_original_ignore_response()
# and we have "new ignore stuff" in ignore_text # and we have "new ignore stuff" in ignore_text
# it is only ignored, it is not removed (it will be highlighted too) # it is only ignored, it is not removed (it will be highlighted too)
assert b'new ignore stuff' in res.data assert b'new ignore stuff' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
# When adding some ignore text, it should not trigger a change, even if something else on that line changes # When adding some ignore text, it should not trigger a change, even if something else on that line changes
@@ -172,7 +172,7 @@ def test_check_global_ignore_text_functionality(client, live_server, measure_mem
# Goto the settings page, add our ignore text # Goto the settings page, add our ignore text
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-ignore_whitespace": "y", "application-ignore_whitespace": "y",
@@ -187,7 +187,7 @@ def test_check_global_ignore_text_functionality(client, live_server, measure_mem
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -198,7 +198,7 @@ def test_check_global_ignore_text_functionality(client, live_server, measure_mem
#Adding some ignore text should not trigger a change #Adding some ignore text should not trigger a change
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"ignore_text": "something irrelevent but just to check", "url": test_url, 'fetch_backend': "html_requests"}, data={"ignore_text": "something irrelevent but just to check", "url": test_url, 'fetch_backend': "html_requests"},
follow_redirects=True follow_redirects=True
) )
@@ -206,12 +206,12 @@ def test_check_global_ignore_text_functionality(client, live_server, measure_mem
# Check it saved # Check it saved
res = client.get( res = client.get(
url_for("settings_page"), url_for("settings.settings_page"),
) )
assert bytes(ignore_text.encode('utf-8')) in res.data assert bytes(ignore_text.encode('utf-8')) in res.data
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
# It should report nothing found (no new 'unviewed' class), adding random ignore text should not cause a change # It should report nothing found (no new 'unviewed' class), adding random ignore text should not cause a change
res = client.get(url_for("index")) res = client.get(url_for("index"))
@@ -224,7 +224,7 @@ def test_check_global_ignore_text_functionality(client, live_server, measure_mem
set_modified_ignore_response() set_modified_ignore_response()
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -236,10 +236,10 @@ def test_check_global_ignore_text_functionality(client, live_server, measure_mem
# Just to be sure.. set a regular modified change that will trigger it # Just to be sure.. set a regular modified change that will trigger it
set_modified_original_ignore_response() set_modified_original_ignore_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -53,7 +53,7 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag
# Goto the settings page, choose to ignore links (dont select/send "application-render_anchor_tag_content") # Goto the settings page, choose to ignore links (dont select/send "application-render_anchor_tag_content")
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-fetch_backend": "html_requests", "application-fetch_backend": "html_requests",
@@ -65,32 +65,32 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag
# Add our URL to the import page # Add our URL to the import page
test_url = url_for("test_endpoint", _external=True) test_url = url_for("test_endpoint", _external=True)
res = client.post( res = client.post(
url_for("import_page"), data={"urls": test_url}, url_for("imports.import_page"), data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# set a new html text with a modified link # set a new html text with a modified link
set_modified_ignore_response() set_modified_ignore_response()
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# We should not see the rendered anchor tag # We should not see the rendered anchor tag
res = client.get(url_for("preview_page", uuid="first")) res = client.get(url_for("ui.ui_views.preview_page", uuid="first"))
assert '(/modified_link)' not in res.data.decode() assert '(/modified_link)' not in res.data.decode()
# Goto the settings page, ENABLE render anchor tag # Goto the settings page, ENABLE render anchor tag
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-render_anchor_tag_content": "true", "application-render_anchor_tag_content": "true",
@@ -101,7 +101,7 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag
assert b"Settings updated." in res.data assert b"Settings updated." in res.data
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
@@ -109,7 +109,7 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag
# check that the anchor tag content is rendered # check that the anchor tag content is rendered
res = client.get(url_for("preview_page", uuid="first")) res = client.get(url_for("ui.ui_views.preview_page", uuid="first"))
assert '(/modified_link)' in res.data.decode() assert '(/modified_link)' in res.data.decode()
# since the link has changed, and we chose to render anchor tag content, # since the link has changed, and we chose to render anchor tag content,
@@ -119,7 +119,7 @@ def test_render_anchor_tag_content_true(client, live_server, measure_memory_usag
assert b"/test-endpoint" in res.data assert b"/test-endpoint" in res.data
# Cleanup everything # Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), res = client.get(url_for("ui.form_delete", uuid="all"),
follow_redirects=True) follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -49,7 +49,7 @@ def test_normal_page_check_works_with_ignore_status_code(client, live_server, me
# Goto the settings page, add our ignore text # Goto the settings page, add our ignore text
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-ignore_status_codes": "y", "application-ignore_status_codes": "y",
@@ -62,7 +62,7 @@ def test_normal_page_check_works_with_ignore_status_code(client, live_server, me
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -73,7 +73,7 @@ def test_normal_page_check_works_with_ignore_status_code(client, live_server, me
set_some_changed_response() set_some_changed_response()
wait_for_all_checks(client) wait_for_all_checks(client)
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)
@@ -96,7 +96,7 @@ def test_403_page_check_works_with_ignore_status_code(client, live_server, measu
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', status_code=403, _external=True) test_url = url_for('test_endpoint', status_code=403, _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -108,7 +108,7 @@ def test_403_page_check_works_with_ignore_status_code(client, live_server, measu
# Goto the edit page, check our ignore option # Goto the edit page, check our ignore option
# Add our URL to the import page # Add our URL to the import page
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("ui.ui_edit.edit_page", uuid="first"),
data={"ignore_status_codes": "y", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"}, data={"ignore_status_codes": "y", "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"},
follow_redirects=True follow_redirects=True
) )
@@ -121,7 +121,7 @@ def test_403_page_check_works_with_ignore_status_code(client, live_server, measu
set_some_changed_response() set_some_changed_response()
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
wait_for_all_checks(client) wait_for_all_checks(client)

View File

@@ -59,7 +59,7 @@ def test_check_ignore_whitespace(client, live_server, measure_memory_usage):
# Goto the settings page, add our ignore text # Goto the settings page, add our ignore text
res = client.post( res = client.post(
url_for("settings_page"), url_for("settings.settings_page"),
data={ data={
"requests-time_between_check-minutes": 180, "requests-time_between_check-minutes": 180,
"application-ignore_whitespace": "y", "application-ignore_whitespace": "y",
@@ -72,7 +72,7 @@ def test_check_ignore_whitespace(client, live_server, measure_memory_usage):
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={"urls": test_url}, data={"urls": test_url},
follow_redirects=True follow_redirects=True
) )
@@ -80,12 +80,12 @@ def test_check_ignore_whitespace(client, live_server, measure_memory_usage):
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
set_original_ignore_response_but_with_whitespace() set_original_ignore_response_but_with_whitespace()
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)

View File

@@ -16,7 +16,7 @@ def test_import(client, live_server, measure_memory_usage):
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={ data={
"distill-io": "", "distill-io": "",
"urls": """https://example.com "urls": """https://example.com
@@ -28,7 +28,7 @@ https://example.com tag1, other tag"""
assert b"3 Imported" in res.data assert b"3 Imported" in res.data
assert b"tag1" in res.data assert b"tag1" in res.data
assert b"other tag" in res.data assert b"other tag" in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
# Clear flask alerts # Clear flask alerts
res = client.get( url_for("index")) res = client.get( url_for("index"))
@@ -41,7 +41,7 @@ def xtest_import_skip_url(client, live_server, measure_memory_usage):
time.sleep(1) time.sleep(1)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={ data={
"distill-io": "", "distill-io": "",
"urls": """https://example.com "urls": """https://example.com
@@ -53,7 +53,7 @@ def xtest_import_skip_url(client, live_server, measure_memory_usage):
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
assert b"ht000000broken" in res.data assert b"ht000000broken" in res.data
assert b"1 Skipped" in res.data assert b"1 Skipped" in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
# Clear flask alerts # Clear flask alerts
res = client.get( url_for("index")) res = client.get( url_for("index"))
@@ -82,9 +82,9 @@ def test_import_distillio(client, live_server, measure_memory_usage):
# Give the endpoint time to spin up # Give the endpoint time to spin up
time.sleep(1) time.sleep(1)
client.get(url_for("form_delete", uuid="all"), follow_redirects=True) client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data={ data={
"distill-io": distill_data, "distill-io": distill_data,
"urls" : '' "urls" : ''
@@ -96,7 +96,7 @@ def test_import_distillio(client, live_server, measure_memory_usage):
assert b"Unable to read JSON file, was it broken?" not in res.data assert b"Unable to read JSON file, was it broken?" not in res.data
assert b"1 Imported from Distill.io" in res.data assert b"1 Imported from Distill.io" in res.data
res = client.get( url_for("edit_page", uuid="first")) res = client.get( url_for("ui.ui_edit.edit_page", uuid="first"))
assert b"https://unraid.net/blog" in res.data assert b"https://unraid.net/blog" in res.data
assert b"Unraid | News" in res.data assert b"Unraid | News" in res.data
@@ -119,7 +119,7 @@ def test_import_distillio(client, live_server, measure_memory_usage):
assert b"nice stuff" in res.data assert b"nice stuff" in res.data
assert b"nerd-news" in res.data assert b"nerd-news" in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
# Clear flask alerts # Clear flask alerts
res = client.get(url_for("index")) res = client.get(url_for("index"))
@@ -146,7 +146,7 @@ def test_import_custom_xlsx(client, live_server, measure_memory_usage):
} }
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data=data, data=data,
follow_redirects=True, follow_redirects=True,
) )
@@ -169,7 +169,7 @@ def test_import_custom_xlsx(client, live_server, measure_memory_usage):
assert filters[0] == '/html[1]/body[1]/div[4]/div[1]/div[1]/div[1]||//*[@id=\'content\']/div[3]/div[1]/div[1]||//*[@id=\'content\']/div[1]' assert filters[0] == '/html[1]/body[1]/div[4]/div[1]/div[1]/div[1]||//*[@id=\'content\']/div[3]/div[1]/div[1]||//*[@id=\'content\']/div[1]'
assert watch.get('time_between_check') == {'weeks': 0, 'days': 1, 'hours': 6, 'minutes': 24, 'seconds': 0} assert watch.get('time_between_check') == {'weeks': 0, 'days': 1, 'hours': 6, 'minutes': 24, 'seconds': 0}
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_import_watchete_xlsx(client, live_server, measure_memory_usage): def test_import_watchete_xlsx(client, live_server, measure_memory_usage):
@@ -186,7 +186,7 @@ def test_import_watchete_xlsx(client, live_server, measure_memory_usage):
} }
res = client.post( res = client.post(
url_for("import_page"), url_for("imports.import_page"),
data=data, data=data,
follow_redirects=True, follow_redirects=True,
) )
@@ -214,5 +214,5 @@ def test_import_watchete_xlsx(client, live_server, measure_memory_usage):
if watch.get('title') == 'system default website': if watch.get('title') == 'system default website':
assert watch.get('fetch_backend') == 'system' # uses default if blank assert watch.get('fetch_backend') == 'system' # uses default if blank
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

View File

@@ -19,7 +19,7 @@ def test_jinja2_in_url_query(client, live_server, measure_memory_usage):
full_url = "{}?{}".format(test_url, full_url = "{}?{}".format(test_url,
"date={% now 'Europe/Berlin', '%Y' %}.{% now 'Europe/Berlin', '%m' %}.{% now 'Europe/Berlin', '%d' %}", ) "date={% now 'Europe/Berlin', '%Y' %}.{% now 'Europe/Berlin', '%m' %}.{% now 'Europe/Berlin', '%d' %}", )
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": full_url, "tags": "test"}, data={"url": full_url, "tags": "test"},
follow_redirects=True follow_redirects=True
) )
@@ -28,7 +28,7 @@ def test_jinja2_in_url_query(client, live_server, measure_memory_usage):
# It should report nothing found (no new 'unviewed' class) # It should report nothing found (no new 'unviewed' class)
res = client.get( res = client.get(
url_for("preview_page", uuid="first"), url_for("ui.ui_views.preview_page", uuid="first"),
follow_redirects=True follow_redirects=True
) )
assert b'date=2' in res.data assert b'date=2' in res.data
@@ -44,7 +44,7 @@ def test_jinja2_security_url_query(client, live_server, measure_memory_usage):
full_url = "{}?{}".format(test_url, full_url = "{}?{}".format(test_url,
"date={{ ''.__class__.__mro__[1].__subclasses__()}}", ) "date={{ ''.__class__.__mro__[1].__subclasses__()}}", )
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("ui.ui_views.form_quick_watch_add"),
data={"url": full_url, "tags": "test"}, data={"url": full_url, "tags": "test"},
follow_redirects=True follow_redirects=True
) )

Some files were not shown because too many files have changed in this diff Show More