1
0
mirror of https://github.com/aquatix/digimarks.git synced 2025-12-06 23:05:10 +01:00

189 Commits

Author SHA1 Message Date
c1024485a3 Updates 2018-11-18 09:54:31 +01:00
9712b269ff More refactoring and some fixes 2018-10-15 17:49:27 +02:00
51b94e4b64 Moving around a lot of stuff 2018-10-15 16:22:58 +02:00
099e6d7ed7 Started refactoring into digimarks package 2018-10-15 15:17:48 +02:00
bba8d86de1 Minor dependency bump 2018-10-05 09:26:26 +02:00
1c5cedf759 Fix misalignment of tag icon 2018-10-05 09:25:41 +02:00
a7c7dae9fc More fields 2018-10-04 22:15:45 +02:00
958ff11a99 Started with refactor to use Vue.js, tags split into list 2018-10-04 21:47:52 +02:00
2615089acd Updates 2018-09-11 13:10:48 +02:00
14bf22f3e5 Updated to 1.0.0 release of MaterializeCSS 2018-09-11 13:10:06 +02:00
e55fb7bd5f Updated some urls 2018-08-29 16:36:23 +02:00
4e1261857d Updates 2018-08-13 10:32:16 +02:00
f9861c1491 Dependency bumps 2018-07-10 22:12:57 +02:00
836077ad12 New MaterializeCSS release 2018-07-10 22:07:45 +02:00
0913ffca2c Minor version bumps 2018-06-13 09:40:13 +02:00
834f95c34b Do not import reserved name 2018-05-25 16:21:53 +02:00
c82e3a02d4 ESLint hint about 'M' being global (MaterializeCSS) 2018-05-25 16:17:29 +02:00
7874002fef pypi downloads is not available any longer 2018-05-25 16:06:36 +02:00
6db0355cc7 Move badge to the same line as the others 2018-05-25 13:10:54 +02:00
Michiel Scholten
0495296f0f Merge pull request #15 from codacy-badger/codacy-badge
Add a Codacy badge to README.rst
2018-05-25 13:09:10 +02:00
The Codacy Badger
a5d225fb56 Add Codacy badge 2018-05-25 10:15:26 +00:00
f6401a3e9f Problems found by Codacy 2018-05-25 12:11:17 +02:00
46e7fc9899 Updates 2018-05-25 12:10:07 +02:00
f6befd0700 Note about themed links 2018-05-22 09:54:09 +02:00
95ff9c01ca Properly theme regular links 2018-05-15 11:51:46 +02:00
b36cd8db6b Also strip enter characters 2018-05-15 11:44:53 +02:00
2b19b770dc Regular links (in tables) should be themed too 2018-05-15 10:58:42 +02:00
6dc74c7102 Autocomplete has been implemented; fix globbering of <systemkey> 2018-05-09 16:07:10 +02:00
ac7906d781 New version of peewee ORM 2018-05-09 16:03:19 +02:00
5d2329ff90 Release candidate of MaterializeCSS 2018-05-09 16:01:37 +02:00
a29e14b7a7 Upgrades 2018-05-03 09:19:33 +02:00
5e4a35527b Fix for bytes/str discrepancy 2018-04-12 15:58:32 +02:00
55609aa353 Dependency updates 2018-04-12 15:52:58 +02:00
0d86c2608a MaterializeCSS update 2018-04-12 15:33:35 +02:00
dd1e3a19ff Run app on 0.0.0.0 instead of 127.0.0.1 (e.g., in termux) 2018-03-30 16:01:50 +02:00
9694ca566b Nicer fallback favicon image 2018-03-30 15:40:29 +02:00
17caef1aed Fix 'home button' link in digimarks title 2018-03-30 09:41:08 +02:00
52a01794f6 Strip unnecessary whitespace 2018-03-30 09:40:53 +02:00
8148a79d28 Fix for narrow favicons if title contains long words 2018-03-30 09:40:19 +02:00
d6e74ff328 'Home' link now takes view type into account 2018-03-28 15:33:51 +02:00
540fd6ba91 Find button now is icon, form size tweaking 2018-03-27 11:15:30 +02:00
3becf27b42 Styling fixes: nice buttons for edit/delete bookmark, button for
list/cards
2018-03-27 09:59:39 +02:00
fe990ecf63 Implemented listing of bookmarks 2018-03-27 09:17:48 +02:00
75080579fd Fix some url lookups 2018-03-26 16:18:59 +02:00
94eb42a882 Note removal of jQuery as a Removed thing 2018-03-26 16:18:52 +02:00
d501c6b4db De-jQuery'd a forgetten piece of Javascript 2018-03-26 15:53:34 +02:00
11e159db8d Re-enable the automatic redirect; some dark styling 2018-03-26 15:42:14 +02:00
6516c4af1d Moved autocompletion items to seperate javascript call 2018-03-26 15:33:07 +02:00
63636d3355 More goodness 2018-03-26 15:18:39 +02:00
52cc93d4c3 Switch default theme to the fresher green variant 2018-03-26 15:11:24 +02:00
6f9d44ce86 Fixed API endpoint for single bookmark lookup 2018-03-26 15:11:07 +02:00
718b39a267 Reminder 2018-03-26 15:00:09 +02:00
bfc4fb702a Some more security against referrers from digimarks 2018-03-26 14:59:49 +02:00
7e2f2f6f6e Support for redirecting a bookmark to its url 2018-03-26 14:58:21 +02:00
29c8c875be More room for delete button (icon stays shown), red colour 2018-03-26 14:25:48 +02:00
0f0caed748 Also theme (autocomplete) search inputfield 2018-03-25 22:22:23 +02:00
971590196e Label, icon and underline are now themed in edit bookmark form 2018-03-25 22:08:37 +02:00
3653b5e424 Disable browser autocomplete for forms, as it interferes 2018-03-25 16:23:56 +02:00
330523ba3f Submit search on autocomplete, limit results in autocomplete 2018-03-24 21:58:53 +01:00
199b641a38 Removed limit for feeds 2018-03-24 21:39:53 +01:00
c0c8e35246 Fix for case where a bookmark has no title 2018-03-24 21:33:15 +01:00
6de9ba2642 Lots of styling fixes, name clash resolutions 2018-03-24 21:31:17 +01:00
e48f2c98c3 Search API endpoint 2018-03-24 21:06:30 +01:00
554f651ec8 Filter autocomplete 2018-03-24 20:48:41 +01:00
6def8d60a5 Buttons and the intention to make collections more accessible 2018-03-24 11:37:35 +01:00
1d531989bb Main nav items to buttons, theme support for buttons 2018-03-24 11:17:26 +01:00
fc2712f5e3 Fix for Python 3 and encoding 2018-03-23 15:10:13 +01:00
76ef520815 Favicon changes and fixes 2018-03-23 14:40:37 +01:00
ea4a7bdcd7 Crash fix 2018-03-23 14:27:47 +01:00
aee0515eae Support for fallback icon, clear empty icon; don't re-download existing 2018-03-23 14:21:15 +01:00
3835497918 Really skip 2018-03-23 13:32:59 +01:00
d7b2c28c96 Case where no icon is supplied 2018-03-23 13:17:21 +01:00
fac3a4f747 Catch case where favicon property is empty 2018-03-23 13:11:06 +01:00
0548f35b39 Endpoint to download all missing icons 2018-03-23 13:03:08 +01:00
8372d6e2a5 Implemented realfavicongenerator API through Mashape 2018-03-23 12:58:27 +01:00
cd2911e7f0 Better description, added missing bs4 dependency 2018-03-23 11:17:16 +01:00
127d99b1e0 We're getting somewhere 2018-03-23 11:08:39 +01:00
bb4f81262e Add icons to the top navbar too 2018-03-23 11:06:02 +01:00
3c697d3162 Find text in url and note too, apart from title 2018-03-23 10:52:09 +01:00
fefb317ddf Use a custom User Agent string to prevent server blockage 2018-03-23 10:44:23 +01:00
37ebdda933 Reference fix 2018-03-17 18:41:27 +01:00
3516fbfbb2 Serialisation fix 2018-03-17 18:41:24 +01:00
c4f921ac68 Refactored the public tag overviews 2018-03-17 18:34:58 +01:00
fec54c51f7 Show tags and edit buttons when on a tag overview page 2018-03-17 18:19:47 +01:00
9f467f8a09 Some more readability fixes 2018-03-17 16:21:31 +01:00
cac31e40c9 Cleanup and better API endpoint 2018-03-17 16:18:53 +01:00
1e14163d42 Lots of indentation and other readability & lintian fixes 2018-03-17 16:14:52 +01:00
88eee28b88 Latest changes 2018-03-17 15:50:49 +01:00
45c95f5f17 Properly serialise bookmarks 2018-03-17 15:46:54 +01:00
9539c48b7b Split fetching of bookmarks into generic and view-specific functions 2018-03-17 15:42:39 +01:00
46aa230fae Support read-only/editable and show/hide tags per view 2018-03-17 15:01:44 +01:00
c6089f1caa Moved tags to the metadata card reveal 2018-03-17 14:56:38 +01:00
45d44d3bdf Moved the cards to their own template 2018-03-17 14:38:49 +01:00
e4662351c2 Non-validating input field 2018-03-17 13:58:58 +01:00
62f3ddf654 Fix for dark-on-dark text in filter input 2018-03-17 13:58:56 +01:00
072ec6c426 Updated dependencies 2018-03-17 13:49:20 +01:00
5055947351 Indentation fix 2018-03-09 15:25:35 +01:00
5f131b15ef Bye bye flask-peewee 2018-03-02 21:10:24 +01:00
bd808a9e1d Reverting to older peewee's, as peewee-flask is hosed 2018-03-02 20:48:23 +01:00
3b019e4368 Updated to the latest MaterializeCSS alpha 2018-03-02 20:24:32 +01:00
3af1239326 Updated dependencies 2018-03-02 20:21:28 +01:00
e8ea948566 Improved ignores 2018-01-12 12:23:58 +01:00
1c2090f300 Cleanups, clarification 2018-01-12 12:21:09 +01:00
cdfd0341f0 Non-validating text inputs 2018-01-03 14:51:54 +01:00
01d6525861 Fixed dark text on dark background 2018-01-03 14:51:35 +01:00
c70e53a658 MaterializeCSS 1.0-alpha-3 2018-01-03 14:40:08 +01:00
dc76a592e0 Updated dependencies 2018-01-03 14:40:03 +01:00
a4aae2d6c4 Note about jQuery being booted 2017-12-25 13:43:24 +01:00
389e63bdbb Removed jQuery code, replaced with pure JavaScript 2017-12-25 11:39:59 +01:00
6ba4803ed2 jQuery -> new MaterializeCSS way 2017-12-24 20:58:15 +01:00
71756f9ea0 Make the Hamburger great again 2017-12-24 14:28:06 +01:00
67635c199a Fixed favicon type sniffing and writing to file in Python 3 2017-12-20 21:01:53 +01:00
7b2a861652 Ch-ch-ch-changes 2017-12-20 20:46:14 +01:00
53887c8ece Also accept http202; try fixing encoding issue 2017-12-19 22:03:51 +01:00
2cc2d382c1 Merge branch 'master' of github.com:aquatix/digimarks 2017-12-19 22:03:16 +01:00
fa033452f1 Fixed checkboxes 2017-12-19 14:58:45 +01:00
c14e24430b Initialise collapsible block so it actually unfolds/folds 2017-12-19 14:55:36 +01:00
cca4504fd7 Updates after Python 3 and latest pip-tools 2017-12-17 15:15:56 +01:00
7f6dc3f3df Python 2+3 2017-12-17 13:52:00 +01:00
4170a7818b Needed for importing settings.py 2017-12-17 13:44:41 +01:00
Michiel Scholten
48e77f551d Merge pull request #13 from aquatix/materialize1.0
Materialize1.0 and Python3
2017-12-17 11:51:24 +01:00
e35d9952bd Not needed with newer pip-tools 2017-12-13 11:47:38 +01:00
6933357a61 Python 3 compatibility fix 2017-12-13 11:47:36 +01:00
9578ee624b Silence 2017-12-13 11:37:26 +01:00
ded047d749 Fixes for MaterializeCSS 1.0.0 changes (based on alpha 2) 2017-12-13 11:37:01 +01:00
57226b88f5 Updates 2017-12-13 11:36:58 +01:00
de5d4d30ef Updated dependencies 2017-11-21 16:18:16 +01:00
60f5a48d89 Black amoled theme 2017-11-21 16:15:50 +01:00
47f6e36e4b Fixed order of imports 2017-11-03 16:55:40 +01:00
5402dfc320 Some updates 2017-11-03 16:53:15 +01:00
90f1322c48 Development requirements 2017-11-03 16:52:48 +01:00
d1aef0284f MaterializeCSS update 2017-08-27 13:49:46 +02:00
d7a5bd921f Recent updates 2017-08-15 10:46:27 +02:00
b85ee43cc7 Better error message on 301, with hint on using the button 2017-08-14 14:24:12 +02:00
8077499eae Something with name clashes 2017-08-14 13:41:58 +02:00
ab06f7e583 Recent changes 2017-07-31 16:34:34 +02:00
f309d4acf2 Make 404 page theme aware (falls back to default) 2017-07-31 16:33:07 +02:00
88a9806d44 Fix clickability of checkboxes 2017-07-31 16:29:27 +02:00
6f4d270858 Don't override the colour of form widgets 2017-07-28 13:49:24 +02:00
c354613b60 Changes 2017-07-26 08:25:47 +02:00
383c77ee8b Fixed padding for card reveal 2017-07-23 14:30:50 +02:00
0e77afd000 Modified card padding so it fits more content 2017-07-23 14:28:20 +02:00
f617fb8190 Moved chip link colour to theme 2017-07-23 13:34:34 +02:00
34af0e9ab7 Added lightblue theme and changed link colour 2017-07-23 13:11:27 +02:00
7f866658e3 (mobile) browser chrome theming 2017-07-23 08:30:26 +02:00
abf9141019 Release 1.1.0 2017-07-22 22:09:42 +02:00
Michiel Scholten
8ca54bf364 Merge pull request #7 from aquatix/theme
Theme
2017-07-22 21:55:03 +02:00
0be05d07e9 Default theme is now a constant; linting fixes 2017-07-22 21:51:07 +02:00
a66360a0e3 Note on the added DB field 2017-07-22 21:44:06 +02:00
1545c17472 Moved label colours to template/theme; added freshgreen and dark 2017-07-22 21:42:12 +02:00
dd0a9d8283 Typo 2017-07-22 21:16:33 +02:00
652e3a89af Theme support, with default 'green' 2017-07-22 21:11:09 +02:00
133a139b79 Merge branch 'master' into theme 2017-07-22 20:36:05 +02:00
b556978640 Updated MaterializeCSS 2017-07-22 19:39:52 +02:00
8bb9e11088 More changes 2017-07-05 12:45:09 +02:00
85e94b71e5 Some reformatting according to keepachangelog.com 2017-07-05 12:32:00 +02:00
a895774001 Changes 2017-07-03 15:08:50 +02:00
7598ba6a61 Updated requirements 2017-07-03 15:07:45 +02:00
c5dcf579d3 Updated MaterializeCSS 2017-07-03 15:07:19 +02:00
590095659e 20170519: default to 'green' 2017-06-15 08:41:01 +02:00
d5d71b4f51 Cache User settings 2017-04-28 16:33:42 +02:00
7f2da49d26 Update Materialize to 0.98.2 2017-04-26 15:22:20 +02:00
0f0e4e03c3 Small requirements updates 2017-04-26 15:20:23 +02:00
044f507aa2 Started with theming support 2017-04-15 09:01:49 +02:00
b0da3a4454 Updated Materialize and jQuery 2017-03-22 12:34:33 +01:00
17e1db12bf Minor requirements updates 2017-03-22 12:32:57 +01:00
480f27797f requirements updates 2017-03-11 21:32:32 +01:00
1729ecd540 Add pkg-resources==0.0.0 as otherwise venv will break 2017-03-01 21:06:59 +01:00
d54b26dafb Updated requirements 2017-03-01 21:03:16 +01:00
2c76ffaf12 Cache buster to force loading of the latest styling 2017-02-04 10:28:07 +01:00
e54a033984 Indentation and styling fixes, comments 2017-02-04 10:27:05 +01:00
4e0d89c25e Better size for the hamburger icon 2017-02-04 10:24:39 +01:00
30e4f74ed9 Fix for misalignment of hamburger icon 2017-02-04 09:20:42 +01:00
1beadb676c Updated MaterializeCSS 2017-02-04 09:20:40 +01:00
eea63398ba Try-out of scrolling tags at 2016-11-07, did not work out yet 2017-02-04 09:18:32 +01:00
d5021fd7c4 Try-out of scrolling tags at 2016-11-07, did not work out yet 2017-02-04 09:18:10 +01:00
c03d0c2458 Recompiled requirements, lowercase package names 2017-01-24 09:48:09 +01:00
ff08ea4930 Explicit requirements through pip-compile from pip-tools 2017-01-16 12:01:08 +01:00
2e9457ee1a Catch case where favicon could not be saved 2017-01-16 11:38:43 +01:00
63dd636c25 Show 404 page if bookmark is not found when editing 2017-01-16 11:22:56 +01:00
412b4a93c7 Fixed indention 2016-12-29 20:59:45 +01:00
abdc11361a Added missing itertools function 2016-12-29 20:59:04 +01:00
Michiel Scholten
7a98de4b3f Merge pull request #3 from jelmer/optional-venv
Make running in a virtualenv optional.
2016-12-29 20:49:16 +01:00
Michiel Scholten
59f5365edc Merge pull request #2 from jelmer/utilkit
Drop dependency on utilkit.
2016-12-29 20:48:42 +01:00
Michiel Scholten
b0aedcd6e4 Merge pull request #1 from jelmer/unique-everseen
Avoid dependency on more_itertools.
2016-12-29 20:47:32 +01:00
6b61e43c5d Upcoming changes 2016-12-29 17:01:55 +01:00
Jelmer Vernooij
570af2d62e Make running in a virtualenv optional. 2016-11-28 23:34:28 +00:00
Jelmer Vernooij
8a19df2741 Drop dependency on utilkit.
utilkit is only used for one one-liner function.
2016-11-28 23:30:51 +00:00
Jelmer Vernooij
9e6d67a0ff Avoid dependency on more_itertools.
Instead, use version of unique_everseen as seen in itertools
documentation.
2016-11-28 23:25:50 +00:00
26 changed files with 923 additions and 948 deletions

10
.gitignore vendored
View File

@@ -87,3 +87,13 @@ ENV/
# Rope project settings
.ropeproject
# vim
*.swp
# digimarks
static/favicons
tags
*.db_*
*.db
settings.py

View File

@@ -1,18 +1,90 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## TODO
- Sorting of bookmarks
- Sort by title
- Sort by date
- Logging of actions
- Change tags to the MaterializeCSS tags: http://materializecss.com/chips.html
- Add new way of authentication and editing bookmark collections:
https://github.com/aquatix/digimarks/issues/8 and https://github.com/aquatix/digimarks/issues/9
- Change adding tags to use the MaterializeCSS tags: https://materializecss.com/chips.html
- Do calls to the API endpoint of an existing bookmark when editing properties
(for example to update tags, title and such, also to already suggest title)
- Look into compatibility with del.icio.us, so we can make use of existing browser integration
- Add unit tests
## v1.0.0
## [Unreleased]
2016-12-29
### Added
- 'lightblue' theme
- 'black amoled' theme
- Python 3 compatibility (tested with Python 3.5 and 3.6)
- Accept 'HTTP 202' responses as 'OK'
- API: Added endpoint for 'bookmarks', returning JSON
- Top navigation items now have icons too, like the sidebar in mobile view
- Download favicons from RealFaviconGenerator: https://realfavicongenerator.net/api/download_website_favicon
- Added `/<systemkey>/findmissingfavicons` endpoint to fill in the blanks in the favicon collection
- Added fallback favicon image (semitransparent digimarks 'M' logo) for bookmarks without a favicon. No more broken images.
- Added theme support for buttons.
- Autocompletion in bookmark search field
- API: search endpoint
- Redirect endpoint for a bookmark, de-referring to its url (`/r/<userkey>/<urlhash>`)
### Changed
- Fixed theming of browser chrome in mobile browsers
- Changed link colour of 'dark' theme from blue to orange
- Modified card padding so it fits more content
- Fixed ability to select a checkbox in the add/edit bookmark form
- Made the 404 page theme aware, falls back to default (green) theme
- Fixed admin pages not working anymore due to `settings` object name clash
- On Add/Edit bookmark and encountering a 301, show a better message about automatically changing the URL with the provided button
- Switched to 1.0 (alpha 4) version of MaterializeCSS
- jQuery-b-gone: changed all jQuery code to regular JavaScript code/MaterializeCSS framework
- Fixed colour of filter text in search field for dark themes
- Unified rendering of 'private' and 'public' views of bookmark cards
- Code cleanups, readability fixes
- digimarks User Agent string to correctly identify ourselves, also preventing servers blocking 'bots'
- Text search now also finds matches in the 'note' and 'url' of a bookmark, aside from its title
- Main navigation items ('tags' and 'add bookmark') are now buttons, better visible as action items.
- Removed item limit for feeds
- Form fields are now themed
- Disabled browser autocomplete for forms, which generally interfered with editing bookmarks (e.g., tag field) and the search field,
which has its own autocomplete now
- Changed default theme to the 'freshgreen' variant
- Links are now themed in the proper colours everywhere
### Removed
- Removed dependency on jQuery
## [1.1.0] - 2017-07-22
### Added
- Show 404 page if bookmark is not found when editing
- Cache buster to force loading of the latest styling
- Theming support, default is 'green'
- Themes need an extra `theme` field in the User table
- Added 'freshgreen' and 'dark' themes
### Changed
- Make running in a virtualenv optional
- Fix for misalignment and size of hamburger icon
- Updated Python (pip) dependencies
- Updated MaterializeCSS and jQuery
### Removed
- Removed dependency on more_itertools
- Removed dependency on utilkit
## [1.0.0] - 2016-12-29
- json view of public tag pages, returns all items
- feed (rss/atom) view of public tag pages, returns latest 15
@@ -47,9 +119,7 @@
- Updated MaterializeCSS and jQuery
## v0.2.0
2016-08-02
## [0.2.0] - 2016-08-02
- Favicon courtesy Freepik on flaticon.com
- Tag tags for easy adding of tags
@@ -60,9 +130,7 @@
- Option to strip parameters from url (like '?utm_source=social')
## v0.1.0
2016-07-26
## [0.1.0] - 2016-07-26
- Initial release
- Flask application with functionality to add users, add and edit bookmarks,

View File

@@ -1,7 +1,7 @@
digimarks
=========
|PyPI version| |PyPI downloads| |PyPI license| |Code health|
|PyPI version| |PyPI license| |Code health| |Codacy|
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching.
@@ -92,13 +92,14 @@ Attributions
.. _webhook: https://en.wikipedia.org/wiki/Webhook
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |PyPI downloads| image:: https://img.shields.io/pypi/dm/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |Code health| image:: https://landscape.io/github/aquatix/digimarks/master/landscape.svg?style=flat
:target: https://landscape.io/github/aquatix/digimarks/master
:alt: Code Health
.. |Codacy| image:: https://api.codacy.com/project/badge/Grade/9a34319d917b43219a29e59e9ac75e3b
:alt: Codacy Badge
:target: https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger
.. _hook settings: https://github.com/aquatix/digimarks/blob/master/example_config/examples.yaml
.. _vhost for Apache2.4: https://github.com/aquatix/digimarks/blob/master/example_config/apache_vhost.conf
.. _uwsgi.ini: https://github.com/aquatix/digimarks/blob/master/example_config/uwsgi.ini

View File

@@ -1,22 +1,30 @@
from __future__ import print_function
import datetime
import gzip
import hashlib
import os
import sys
import requests
import shutil
import sys
import bs4
from more_itertools import unique_everseen
from urlparse import urlparse, urlunparse, urljoin
from utilkit import datetimeutil
from flask import Flask, abort, redirect, render_template, request, url_for, jsonify
import requests
from flask import (Flask, abort, jsonify, redirect, render_template, request,
url_for)
from peewee import * # noqa
from werkzeug.contrib.atom import AtomFeed
from flask_peewee.db import Database
#from flask_peewee.utils import get_object_or_404
from peewee import * # noqa
from digimarks import models, themes, views
try:
# Python 3
from urllib.parse import urljoin, urlparse, urlunparse
except ImportError:
# Python 2
from urlparse import urljoin, urlparse, urlunparse
DIGIMARKS_USER_AGENT = 'digimarks/1.2.0-dev'
try:
import settings
@@ -28,17 +36,14 @@ except ImportError:
APP_ROOT = os.path.dirname(os.path.realpath(__file__))
MEDIA_ROOT = os.path.join(APP_ROOT, 'static')
MEDIA_URL = '/static/'
DATABASE = {
'name': os.path.join(APP_ROOT, 'bookmarks.db'),
'engine': 'peewee.SqliteDatabase',
}
#PHANTOM = '/usr/local/bin/phantomjs'
#SCRIPT = os.path.join(APP_ROOT, 'screenshot.js')
# create our flask app and a database wrapper
app = Flask(__name__)
app.config.from_object(__name__)
db = Database(app)
# Strip unnecessary whitespace due to jinja2 codeblocks
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# set custom url for the app, for example '/bookmarks'
try:
@@ -48,217 +53,7 @@ except AttributeError:
# Cache the tags
all_tags = {}
def clean_tags(tags_list):
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_everseen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
magic_dict = {
"\x1f\x8b\x08": "gz",
"\x42\x5a\x68": "bz2",
"\x50\x4b\x03\x04": "zip"
}
max_len = max(len(x) for x in magic_dict)
def file_type(filename):
with open(filename) as f:
file_start = f.read(max_len)
for magic, filetype in magic_dict.items():
if file_start.startswith(magic):
return filetype
return "no match"
class User(db.Model):
""" User account """
username = CharField()
key = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = os.urandom(24).encode('hex')
return self.key
class Bookmark(db.Model):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
note = TextField(default='')
#image = CharField(default='')
url_hash = CharField(default='')
tags = CharField(default='')
starred = BooleanField(default=False)
# Website (domain) favicon
favicon = CharField(null=True)
# Status code: 200 is OK, 404 is not found, for example (showing an error)
HTTP_CONNECTIONERROR = 0
HTTP_OK = 200
HTTP_MOVEDTEMPORARILY = 304
HTTP_NOTFOUND = 404
http_status = IntegerField(default=200)
redirect_uri = None
created_date = DateTimeField(default=datetime.datetime.now)
modified_date = DateTimeField(null=True)
deleted_date = DateTimeField(null=True)
# Bookmark status; deleting doesn't remove from DB
VISIBLE = 0
DELETED = 1
status = IntegerField(default=VISIBLE)
class Meta:
ordering = (('created_date', 'desc'),)
#def fetch_image(self):
# url_hash = hashlib.md5(self.url).hexdigest()
# filename = 'bookmark-%s.png' % url_hash
# outfile = os.path.join(MEDIA_ROOT, filename)
# params = [PHANTOM, SCRIPT, self.url, outfile]
# exitcode = subprocess.call(params)
# if exitcode == 0:
# self.image = os.path.join(MEDIA_URL, filename)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url)
self.http_status = result.status_code
except:
# For example 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
self.http_status = 404
if self.http_status == 200:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
self.title = html.title.text.strip()
except AttributeError:
self.title = ''
return self.title
def set_status_code(self):
""" Check the HTTP status of the url, as it might not exist for example """
try:
result = requests.head(self.url)
self.http_status = result.status_code
except requests.ConnectionError:
self.http_status = self.HTTP_CONNECTIONERROR
return self.http_status
def set_favicon(self):
""" Fetch favicon for the domain """
# http://codingclues.eu/2009/retrieve-the-favicon-for-any-url-thanks-to-google/
u = urlparse(self.url)
domain = u.netloc
# if file exists, don't re-download it
#response = requests.get('http://www.google.com/s2/favicons?domain=' + domain, stream=True)
fileextension = '.png'
meta = requests.head('http://icons.better-idea.org/icon?size=60&url=' + domain, allow_redirects=True)
if meta.url[-3:].lower() == 'ico':
fileextension = '.ico'
response = requests.get('http://icons.better-idea.org/icon?size=60&url=' + domain, stream=True)
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
new = file(filename, 'wb')
new.write(origcontent)
new.close()
self.favicon = domain + fileextension
def set_tags(self, tags):
""" Set tags from `tags`, strip and sort them """
tags_split = tags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
def get_redirect_uri(self):
if self.redirect_uri:
return self.redirect_uri
if self.http_status == 301 or self.http_status == 302:
result = requests.head(self.url, allow_redirects=True)
self.http_status = result.status_code
self.redirect_uri = result.url
return result.url
else:
return None
def get_uri_domain(self):
parsed = urlparse(self.url)
return parsed.hostname
@classmethod
def strip_url_params(cls, url):
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
@property
def tags_list(self):
""" Get the tags as a list, iterable in template """
if self.tags:
return self.tags.split(',')
else:
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': datetimeutil.datetime_to_string(self.created_date),
'url_hash': self.url_hash,
'tags': self.tags,
}
return result
class PublicTag(db.Model):
""" Publicly shared tag """
tagkey = CharField()
userkey = CharField()
tag = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate hash-based key for publicly shared tag """
self.tagkey = os.urandom(16).encode('hex')
def get_tags_for_user(userkey):
""" Extract all tags from the bookmarks """
bookmarks = Bookmark.select().filter(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)
usersettings = {}
def get_cached_tags(userkey):
@@ -269,25 +64,44 @@ def get_cached_tags(userkey):
return []
def get_theme(userkey):
try:
usertheme = usersettings[userkey]['theme']
return themes[usertheme]
except KeyError:
return themes[DEFAULT_THEME] # default
def make_external(url):
return urljoin(request.url_root, url)
return urljoin(request.url_root, url)
def _find_bookmarks(userkey, filter_text):
return Bookmark.select().where(
Bookmark.userkey == userkey,
(
Bookmark.title.contains(filter_text) |
Bookmark.url.contains(filter_text) |
Bookmark.note.contains(filter_text)
),
Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc())
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html', error=e), 404
theme = themes[DEFAULT_THEME]
return render_template('404.html', error=e, theme=theme), 404
@app.route('/')
def index():
""" Homepage, point visitors to project page """
return render_template('index.html')
theme = themes[DEFAULT_THEME]
return render_template('index.html', theme=theme)
@app.route('/<userkey>', methods=['GET', 'POST'])
@app.route('/<userkey>/filter/<filtermethod>', methods=['GET', 'POST'])
@app.route('/<userkey>/sort/<sortmethod>', methods=['GET', 'POST'])
def bookmarks(userkey, filtermethod = None, sortmethod = None):
def get_bookmarks(userkey, filtermethod=None, sortmethod=None):
""" User homepage, list their bookmarks, optionally filtered and/or sorted """
#return object_list('bookmarks.html', Bookmark.select())
#user = User.select(key=userkey)
@@ -297,7 +111,7 @@ def bookmarks(userkey, filtermethod = None, sortmethod = None):
#else:
# abort(404)
message = request.args.get('message')
tags = get_cached_tags(userkey)
bookmarktags = get_cached_tags(userkey)
filter_text = ''
if request.form:
@@ -316,11 +130,10 @@ def bookmarks(userkey, filtermethod = None, sortmethod = None):
filter_note = True
if filter_text:
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.title.contains(filter_text),
Bookmark.status == Bookmark.VISIBLE).order_by(Bookmark.created_date.desc())
bookmarks = _find_bookmarks(userkey, filter_text)
elif filter_starred:
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey,
Bookmark.starred == True).order_by(Bookmark.created_date.desc())
Bookmark.starred).order_by(Bookmark.created_date.desc())
elif filter_broken:
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey,
Bookmark.http_status != 200).order_by(Bookmark.created_date.desc())
@@ -328,24 +141,107 @@ def bookmarks(userkey, filtermethod = None, sortmethod = None):
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey,
Bookmark.note != '').order_by(Bookmark.created_date.desc())
else:
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE).order_by(Bookmark.created_date.desc())
bookmarks = Bookmark.select().where(
Bookmark.userkey == userkey,
Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc())
return render_template('bookmarks.html', bookmarks=bookmarks, userkey=userkey, tags=tags, filter_text=filter_text, message=message)
return bookmarks, bookmarktags, filter_text, message
#@app.route('/<userkey>/<urlhash>')
#def viewbookmark(userkey, urlhash):
# """ Bookmark detail view """
# bookmark = Bookmark.select(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey)
# return render_template('viewbookmark.html', userkey=userkey, bookmark=bookmark)
@app.route('/<userkey>', methods=['GET', 'POST'])
@app.route('/<userkey>/filter/<filtermethod>', methods=['GET', 'POST'])
@app.route('/<userkey>/sort/<sortmethod>', methods=['GET', 'POST'])
@app.route('/<userkey>/<show_as>', methods=['GET', 'POST'])
@app.route('/<userkey>/<show_as>/filter/<filtermethod>', methods=['GET', 'POST'])
@app.route('/<userkey>/<show_as>/sort/<sortmethod>', methods=['GET', 'POST'])
def bookmarks_page(userkey, filtermethod=None, sortmethod=None, show_as='cards'):
bookmarks, bookmarktags, filter_text, message = get_bookmarks(userkey, filtermethod, sortmethod)
theme = get_theme(userkey)
return render_template(
'bookmarks.html',
bookmarks=bookmarks,
userkey=userkey,
tags=bookmarktags,
filter_text=filter_text,
message=message,
theme=theme,
editable=True, # bookmarks can be edited
showtags=True, # tags should be shown with the bookmarks
filtermethod=filtermethod,
sortmethod=sortmethod,
show_as=show_as, # show list of bookmarks instead of cards
)
@app.route('/<userkey>/<urlhash>/json')
def viewbookmarkjson(userkey, urlhash):
@app.route('/<userkey>/js')
def bookmarks_js(userkey):
""" Return list of bookmarks with their favicons, to be used for autocompletion """
bookmarks = Bookmark.select().where(
Bookmark.userkey == userkey,
Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc())
return render_template(
'bookmarks.js',
bookmarks=bookmarks
)
@app.route('/r/<userkey>/<urlhash>')
def bookmark_redirect(userkey, urlhash):
""" Securely redirect a bookmark to its url, stripping referrer (if browser plays nice) """
# @TODO: add counter to this bookmark
try:
bookmark = Bookmark.get(
Bookmark.url_hash == urlhash,
Bookmark.userkey == userkey,
Bookmark.status == Bookmark.VISIBLE
)
except Bookmark.DoesNotExist:
abort(404)
return render_template('redirect.html', url=bookmark.url)
@app.route('/api/v1/<userkey>', methods=['GET', 'POST'])
@app.route('/api/v1/<userkey>/filter/<filtermethod>', methods=['GET', 'POST'])
@app.route('/api/v1/<userkey>/sort/<sortmethod>', methods=['GET', 'POST'])
def bookmarks_json(userkey, filtermethod=None, sortmethod=None):
bookmarks, bookmarktags, filter_text, message = get_bookmarks(userkey, filtermethod, sortmethod)
bookmarkslist = [i.serialize for i in bookmarks]
the_data = {
'bookmarks': bookmarkslist,
'tags': bookmarktags,
'filter_text': filter_text,
'message': message,
'userkey': userkey,
}
return jsonify(the_data)
@app.route('/api/v1/<userkey>/<urlhash>')
def bookmark_json(userkey, urlhash):
""" Serialise bookmark to json """
bookmark = Bookmark.select(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)[0]
return jsonify(bookmark.to_dict())
try:
bookmark = Bookmark.get(
Bookmark.url_hash == urlhash,
Bookmark.userkey == userkey,
Bookmark.status == Bookmark.VISIBLE
)
return jsonify(bookmark.to_dict())
except Bookmark.DoesNotExist:
return jsonify({'message': 'Bookmark not found', 'status': 'error 404'})
@app.route('/api/v1/<userkey>/search/<filter_text>')
def search_bookmark_titles_json(userkey, filter_text):
""" Serialise bookmark to json """
bookmarks = _find_bookmarks(userkey, filter_text)
result = []
for bookmark in bookmarks:
result.append(bookmark.to_dict())
return jsonify(result)
@app.route('/<userkey>/<urlhash>')
@@ -353,13 +249,26 @@ def viewbookmarkjson(userkey, urlhash):
def editbookmark(userkey, urlhash):
""" Bookmark edit form """
# bookmark = getbyurlhash()
bookmark = Bookmark.get(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey)
try:
bookmark = Bookmark.get(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey)
except Bookmark.DoesNotExist:
abort(404)
message = request.args.get('message')
tags = get_cached_tags(userkey)
if not bookmark.note:
# Workaround for when an existing bookmark has a null note
bookmark.note = ''
return render_template('edit.html', action='Edit bookmark', userkey=userkey, bookmark=bookmark, message=message, formaction='edit', tags=tags)
theme = get_theme(userkey)
return render_template(
'edit.html',
action='Edit bookmark',
userkey=userkey,
bookmark=bookmark,
message=message,
formaction='edit',
tags=tags,
theme=theme
)
@app.route('/<userkey>/add')
@@ -373,10 +282,19 @@ def addbookmark(userkey):
bookmark = Bookmark(title='', url=url, tags='')
message = request.args.get('message')
tags = get_cached_tags(userkey)
return render_template('edit.html', action='Add bookmark', userkey=userkey, bookmark=bookmark, tags=tags, message=message)
theme = get_theme(userkey)
return render_template(
'edit.html',
action='Add bookmark',
userkey=userkey,
bookmark=bookmark,
tags=tags,
message=message,
theme=theme
)
def updatebookmark(userkey, request, urlhash = None):
def updatebookmark(userkey, urlhash=None):
""" Add (no urlhash) or edit (urlhash is set) a bookmark """
title = request.form.get('title')
url = request.form.get('url')
@@ -419,8 +337,12 @@ def updatebookmark(userkey, request, urlhash = None):
else:
bookmark.set_status_code()
if bookmark.http_status == 200:
bookmark.set_favicon()
if bookmark.http_status == 200 or bookmark.http_status == 202:
try:
bookmark.set_favicon()
except IOError:
# Icon file could not be saved possibly, don't bail completely
pass
bookmark.save()
return bookmark
@@ -433,7 +355,7 @@ def addingbookmark(userkey):
tags = get_cached_tags(userkey)
if request.method == 'POST':
bookmark = updatebookmark(userkey, request)
bookmark = updatebookmark(userkey)
if not bookmark:
return redirect(url_for('addbookmark', userkey=userkey, message='No url provided', tags=tags))
if type(bookmark).__name__ == 'Response':
@@ -448,7 +370,7 @@ def editingbookmark(userkey, urlhash):
""" Edit the bookmark from form submit """
if request.method == 'POST':
bookmark = updatebookmark(userkey, request, urlhash=urlhash)
bookmark = updatebookmark(userkey, urlhash=urlhash)
all_tags[userkey] = get_tags_for_user(userkey)
return redirect(url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash))
return redirect(url_for('editbookmark', userkey=userkey, urlhash=urlhash))
@@ -457,27 +379,34 @@ def editingbookmark(userkey, urlhash):
@app.route('/<userkey>/<urlhash>/delete', methods=['GET', 'POST'])
def deletingbookmark(userkey, urlhash):
""" Delete the bookmark from form submit by <urlhash>/delete """
query = Bookmark.update(status=Bookmark.DELETED).where(Bookmark.userkey==userkey, Bookmark.url_hash==urlhash)
query = Bookmark.update(status=Bookmark.DELETED).where(Bookmark.userkey == userkey, Bookmark.url_hash == urlhash)
query.execute()
query = Bookmark.update(deleted_date = datetime.datetime.now()).where(Bookmark.userkey==userkey, Bookmark.url_hash==urlhash)
query = Bookmark.update(deleted_date=datetime.datetime.now()).where(
Bookmark.userkey == userkey,
Bookmark.url_hash == urlhash
)
query.execute()
message = 'Bookmark deleted. <a href="{}">Undo deletion</a>'.format(url_for('undeletebookmark', userkey=userkey, urlhash=urlhash))
message = 'Bookmark deleted. <a href="{}">Undo deletion</a>'.format(url_for(
'undeletebookmark',
userkey=userkey,
urlhash=urlhash
))
all_tags[userkey] = get_tags_for_user(userkey)
return redirect(url_for('bookmarks', userkey=userkey, message=message))
return redirect(url_for('bookmarks_page', userkey=userkey, message=message))
@app.route('/<userkey>/<urlhash>/undelete')
def undeletebookmark(userkey, urlhash):
""" Undo deletion of the bookmark identified by urlhash """
query = Bookmark.update(status=Bookmark.VISIBLE).where(Bookmark.userkey==userkey, Bookmark.url_hash==urlhash)
query = Bookmark.update(status=Bookmark.VISIBLE).where(Bookmark.userkey == userkey, Bookmark.url_hash == urlhash)
query.execute()
message = 'Bookmark restored'
all_tags[userkey] = get_tags_for_user(userkey)
return redirect(url_for('bookmarks', userkey=userkey, message=message))
return redirect(url_for('bookmarks_page', userkey=userkey, message=message))
@app.route('/<userkey>/tags')
def tags(userkey):
def tags_page(userkey):
""" Overview of all tags used by user """
tags = get_cached_tags(userkey)
#publictags = PublicTag.select().where(Bookmark.userkey == userkey)
@@ -488,24 +417,43 @@ def tags(userkey):
except PublicTag.DoesNotExist:
publictag = None
total = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.tags.contains(tag), Bookmark.status == Bookmark.VISIBLE).count()
total = Bookmark.select().where(
Bookmark.userkey == userkey,
Bookmark.tags.contains(tag),
Bookmark.status == Bookmark.VISIBLE
).count()
alltags.append({'tag': tag, 'publictag': publictag, 'total': total})
totaltags = len(alltags)
totalbookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE).count()
totalpublic = PublicTag.select().where(PublicTag.userkey == userkey).count()
totalstarred = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.starred == True).count()
totalstarred = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.starred).count()
totaldeleted = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.status == Bookmark.DELETED).count()
totalnotes = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.note != '').count()
totalhttperrorstatus = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.http_status != 200).count()
return render_template('tags.html', tags=alltags, totaltags=totaltags, totalpublic=totalpublic, totalbookmarks=totalbookmarks,
totaldeleted=totaldeleted, totalstarred=totalstarred, totalhttperrorstatus=totalhttperrorstatus,
totalnotes=totalnotes, userkey=userkey)
theme = get_theme(userkey)
return render_template(
'tags.html',
tags=alltags,
totaltags=totaltags,
totalpublic=totalpublic,
totalbookmarks=totalbookmarks,
totaldeleted=totaldeleted,
totalstarred=totalstarred,
totalhttperrorstatus=totalhttperrorstatus,
totalnotes=totalnotes,
userkey=userkey,
theme=theme
)
@app.route('/<userkey>/tag/<tag>')
def tag(userkey, tag):
def tag_page(userkey, tag):
""" Overview of all bookmarks with a certain tag """
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.tags.contains(tag), Bookmark.status == Bookmark.VISIBLE).order_by(Bookmark.created_date.desc())
bookmarks = Bookmark.select().where(
Bookmark.userkey == userkey,
Bookmark.tags.contains(tag),
Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc())
tags = get_cached_tags(userkey)
pageheader = 'tag: ' + tag
message = request.args.get('message')
@@ -515,28 +463,63 @@ def tag(userkey, tag):
except PublicTag.DoesNotExist:
publictag = None
return render_template('bookmarks.html', bookmarks=bookmarks, userkey=userkey, tags=tags, tag=tag, publictag=publictag, action=pageheader, message=message)
theme = get_theme(userkey)
return render_template(
'bookmarks.html',
bookmarks=bookmarks,
userkey=userkey,
tags=tags,
tag=tag,
publictag=publictag,
action=pageheader,
message=message,
theme=theme,
editable=True,
showtags=True,
)
def get_publictag(tagkey):
""" Return tag and bookmarks in this public tag collection """
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
bookmarks = Bookmark.select().where(
Bookmark.userkey == this_tag.userkey,
Bookmark.tags.contains(this_tag.tag),
Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc())
return this_tag, bookmarks
@app.route('/pub/<tagkey>')
def publictag(tagkey):
def publictag_page(tagkey):
""" Read-only overview of the bookmarks in the userkey/tag of this PublicTag """
#this_tag = get_object_or_404(PublicTag.select().where(PublicTag.tagkey == tagkey))
try:
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
bookmarks = Bookmark.select().where(Bookmark.userkey == this_tag.userkey, Bookmark.tags.contains(this_tag.tag), Bookmark.status == Bookmark.VISIBLE).order_by(Bookmark.created_date.desc())
return render_template('publicbookmarks.html', bookmarks=bookmarks, tag=tag, action=this_tag.tag, tagkey=tagkey)
this_tag, bookmarks = get_publictag(tagkey)
theme = themes[DEFAULT_THEME]
return render_template(
'publicbookmarks.html',
bookmarks=bookmarks,
tag=this_tag.tag,
action=this_tag.tag,
tagkey=tagkey,
theme=theme
)
except PublicTag.DoesNotExist:
abort(404)
@app.route('/pub/<tagkey>/json')
def publictagjson(tagkey):
@app.route('/api/v1/pub/<tagkey>')
def publictag_json(tagkey):
""" json representation of the Read-only overview of the bookmarks in the userkey/tag of this PublicTag """
try:
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
bookmarks = Bookmark.select().where(Bookmark.userkey == this_tag.userkey, Bookmark.tags.contains(this_tag.tag), Bookmark.status == Bookmark.VISIBLE)
result = {'count': len(bookmarks), 'items': []}
this_tag, bookmarks = get_publictag(tagkey)
result = {
#'tag': this_tag,
'tagkey': tagkey,
'count': len(bookmarks),
'items': [],
}
for bookmark in bookmarks:
result['items'].append(bookmark.to_dict())
return jsonify(result)
@@ -545,22 +528,31 @@ def publictagjson(tagkey):
@app.route('/pub/<tagkey>/feed')
def publictagfeed(tagkey):
def publictag_feed(tagkey):
""" rss/atom representation of the Read-only overview of the bookmarks in the userkey/tag of this PublicTag """
try:
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
bookmarks = Bookmark.select().where(Bookmark.userkey == this_tag.userkey, Bookmark.tags.contains(this_tag.tag), Bookmark.status == Bookmark.VISIBLE).limit(15)
feed = AtomFeed(this_tag.tag, feed_url=request.url, url=make_external(url_for('publictag', tagkey=tagkey)))
bookmarks = Bookmark.select().where(
Bookmark.userkey == this_tag.userkey,
Bookmark.tags.contains(this_tag.tag),
Bookmark.status == Bookmark.VISIBLE
)
feed = AtomFeed(this_tag.tag, feed_url=request.url, url=make_external(url_for('publictag_page', tagkey=tagkey)))
for bookmark in bookmarks:
updated_date = bookmark.modified_date
if not bookmark.modified_date:
updated_date = bookmark.created_date
feed.add(bookmark.title,
content_type='html',
author='digimarks',
url=bookmark.url,
updated=updated_date,
published=bookmark.created_date)
bookmarktitle = '{} (no title)'.format(bookmark.url)
if bookmark.title:
bookmarktitle = bookmark.title
feed.add(
bookmarktitle,
content_type='html',
author='digimarks',
url=bookmark.url,
updated=updated_date,
published=bookmark.created_date
)
return feed.get_response()
except PublicTag.DoesNotExist:
abort(404)
@@ -585,10 +577,10 @@ def addpublictag(userkey, tag):
newpublictag.save()
message = 'Public link to this tag created'
return redirect(url_for('tag', userkey=userkey, tag=tag, message=message))
else:
message = 'Public link already existed'
return redirect(url_for('tag', userkey=userkey, tag=tag, message=message))
return redirect(url_for('tag_page', userkey=userkey, tag=tag, message=message))
message = 'Public link already existed'
return redirect(url_for('tag_page', userkey=userkey, tag=tag, message=message))
@app.route('/<userkey>/<tag>/removepublic/<tagkey>', methods=['GET', 'POST'])
@@ -596,7 +588,7 @@ def removepublictag(userkey, tag, tagkey):
q = PublicTag.delete().where(PublicTag.userkey == userkey, PublicTag.tag == tag, PublicTag.tagkey == tagkey)
q.execute()
message = 'Public link deleted'
return redirect(url_for('tag', userkey=userkey, tag=tag, message=message))
return redirect(url_for('tag_page', userkey=userkey, tag=tag, message=message))
@app.route('/<systemkey>/adduser')
@@ -608,7 +600,7 @@ def adduser(systemkey):
newuser.username = 'Nomen Nescio'
newuser.save()
all_tags[newuser.key] = []
return redirect('/' + newuser.key, code=302)
return redirect('/{}'.format(newuser.key.decode("utf-8")), code=302)
else:
abort(404)
@@ -631,6 +623,28 @@ def refreshfavicons(systemkey):
abort(404)
@app.route('/<systemkey>/findmissingfavicons')
def findmissingfavicons(systemkey):
""" Add user endpoint, convenience """
if systemkey == settings.SYSTEMKEY:
bookmarks = Bookmark.select()
for bookmark in bookmarks:
try:
if not bookmark.favicon or not os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + bookmark.favicon)):
# This favicon is missing
# Clear favicon, so fallback can be used instead of showing a broken image
bookmark.favicon = None
bookmark.save()
# Try to fetch and save new favicon
bookmark.set_favicon()
bookmark.save()
except OSError as e:
print(e)
return redirect('/')
else:
abort(404)
# Initialisation == create the bookmark, user and public tag tables if they do not exist
Bookmark.create_table(True)
User.create_table(True)
@@ -640,9 +654,10 @@ users = User.select()
print('Current user keys:')
for user in users:
all_tags[user.key] = get_tags_for_user(user.key)
usersettings[user.key] = {'theme': user.theme}
print(user.key)
# Run when called standalone
if __name__ == '__main__':
# run the application
app.run(port=9999, debug=True)
app.run(host='0.0.0.0', port=9999, debug=True)

0
digimarks/__init__.py Normal file
View File

321
digimarks/models.py Normal file
View File

@@ -0,0 +1,321 @@
"""digimarks data models and accompanying convenience functions"""
import binascii
import os
import datetime
from peewee import * # noqa
from . import themes
try:
# Python 3
from urllib.parse import urljoin, urlparse, urlunparse
except ImportError:
# Python 2
from urlparse import urljoin, urlparse, urlunparse
DATABASE_PATH = os.path.dirname(os.path.realpath(__file__))
if 'DIGIMARKS_DB_PATH' in os.environ:
DATABASE_PATH = os.environ['DIGIMARKS_DB_PATH']
database = SqliteDatabase(os.path.join(DATABASE_PATH, 'bookmarks.db'))
def ifilterfalse(predicate, iterable):
# ifilterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
if predicate is None:
predicate = bool
for x in iterable:
if not predicate(x):
yield x
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in ifilterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
def clean_tags(tags_list):
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_everseen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
magic_dict = {
b"\x1f\x8b\x08": "gz",
b"\x42\x5a\x68": "bz2",
b"\x50\x4b\x03\x04": "zip"
}
max_len = max(len(x) for x in magic_dict)
def file_type(filename):
with open(filename, "rb") as f:
file_start = f.read(max_len)
for magic, filetype in magic_dict.items():
if file_start.startswith(magic):
return filetype
return "no match"
class BaseModel(Model):
class Meta:
database = database
class User(BaseModel):
""" User account """
username = CharField()
key = CharField()
theme = CharField(default=themes.DEFAULT_THEME)
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = binascii.hexlify(os.urandom(24))
return self.key
class Bookmark(BaseModel):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
note = TextField(default='')
#image = CharField(default='')
url_hash = CharField(default='')
tags = CharField(default='')
starred = BooleanField(default=False)
# Website (domain) favicon
favicon = CharField(null=True)
# Status code: 200 is OK, 404 is not found, for example (showing an error)
HTTP_CONNECTIONERROR = 0
HTTP_OK = 200
HTTP_ACCEPTED = 202
HTTP_MOVEDTEMPORARILY = 304
HTTP_NOTFOUND = 404
http_status = IntegerField(default=200)
redirect_uri = None
created_date = DateTimeField(default=datetime.datetime.now)
modified_date = DateTimeField(null=True)
deleted_date = DateTimeField(null=True)
# Bookmark status; deleting doesn't remove from DB
VISIBLE = 0
DELETED = 1
status = IntegerField(default=VISIBLE)
class Meta:
ordering = (('created_date', 'desc'),)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url.encode('utf-8')).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except:
# For example 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
self.http_status = 404
if self.http_status == 200 or self.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
self.title = html.title.text.strip()
except AttributeError:
self.title = ''
return self.title
def set_status_code(self):
""" Check the HTTP status of the url, as it might not exist for example """
try:
result = requests.head(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except requests.ConnectionError:
self.http_status = self.HTTP_CONNECTIONERROR
return self.http_status
def _set_favicon_with_iconsbetterideaorg(self, domain):
""" Fetch favicon for the domain """
fileextension = '.png'
meta = requests.head(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
allow_redirects=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
if meta.url[-3:].lower() == 'ico':
fileextension = '.ico'
response = requests.get(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def _set_favicon_with_realfavicongenerator(self, domain):
""" Fetch favicon for the domain """
response = requests.get(
'https://realfavicongenerator.p.mashape.com/favicon/icon?platform=android_chrome&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
if response.status_code == 404:
# Fall back to desktop favicon
response = requests.get(
'https://realfavicongenerator.p.mashape.com/favicon/icon?platform=desktop&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
# Debug for the moment
print(domain)
print(response.headers)
if 'Content-Length' in response.headers and response.headers['Content-Length'] == '0':
# No favicon found, likely
print('Skipping this favicon, needs fallback')
return
# Default to 'image/png'
fileextension = '.png'
if response.headers['content-type'] == 'image/jpeg':
fileextension = '.jpg'
if response.headers['content-type'] == 'image/x-icon':
fileextension = '.ico'
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def set_favicon(self):
""" Fetch favicon for the domain """
u = urlparse(self.url)
domain = u.netloc
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')):
# If file exists, don't re-download it
self.favicon = domain + '.png'
return
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.ico')):
# If file exists, don't re-download it
self.favicon = domain + '.ico'
return
#self._set_favicon_with_iconsbetterideaorg(domain)
self._set_favicon_with_realfavicongenerator(domain)
def set_tags(self, newtags):
""" Set tags from `tags`, strip and sort them """
tags_split = newtags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
def get_redirect_uri(self):
if self.redirect_uri:
return self.redirect_uri
if self.http_status == 301 or self.http_status == 302:
result = requests.head(self.url, allow_redirects=True, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
self.redirect_uri = result.url
return result.url
return None
def get_uri_domain(self):
parsed = urlparse(self.url)
return parsed.hostname
@classmethod
def strip_url_params(cls, url):
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
@property
def tags_list(self):
""" Get the tags as a list, iterable in template """
if self.tags:
return self.tags.split(',')
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': self.created_date.strftime('%Y-%m-%d %H:%M:%S'),
'url_hash': self.url_hash,
'tags': self.tags.split(','),
'favicon': self.favicon,
'http_status': self.http_status,
'redirect_uri': self.redirect_uri,
}
return result
@property
def serialize(self):
return self.to_dict()
class PublicTag(BaseModel):
""" Publicly shared tag """
tagkey = CharField()
userkey = CharField()
tag = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate hash-based key for publicly shared tag """
self.tagkey = binascii.hexlify(os.urandom(16))
def get_tags_for_user(userkey):
""" Extract all tags from the bookmarks """
bookmarks = Bookmark.select().filter(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)

126
digimarks/themes.py Normal file
View File

@@ -0,0 +1,126 @@
"""digimarks theme definitions"""
DEFAULT_THEME = 'freshgreen'
themes = {
'green': {
'BROWSERCHROME': '#2e7d32', # green darken-2
'BODY': 'grey lighten-4',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-3',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-3',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'freshgreen': {
'BROWSERCHROME': '#43a047', # green darken-1
'BODY': 'grey lighten-5',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-1',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-1',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'lightblue': {
'BROWSERCHROME': '#0288d1', # light-blue darken-2
'BODY': 'white',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'light-blue darken-2',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#FFF', # white
'CARD_BACKGROUND': 'light-blue lighten-2',
'CARD_TEXT': 'black-text',
'CARD_LINK': '#263238', # blue-grey-text darken-4
'CHIP_TEXT': '#FFF', # white
'FAB': 'light-blue darken-4',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'dark': {
'BROWSERCHROME': '#212121', # grey darken-4
'BODY': 'grey darken-4',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'amoled': {
'BROWSERCHROME': '#000', # grey darken-4
'BODY': 'black',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
}
}

2
digimarks/views.py Normal file
View File

@@ -0,0 +1,2 @@
"""digimarks views"""

View File

@@ -1,4 +1,4 @@
# Virtualenv to use with the wsgi file
# Virtualenv to use with the wsgi file (optional)
VENV = '/srv/marks.example.com/venv/bin/activate_this.py'
PORT = 8086

3
requirements-dev.in Normal file
View File

@@ -0,0 +1,3 @@
-r requirements.in
pylint

27
requirements-dev.txt Normal file
View File

@@ -0,0 +1,27 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements-dev.txt requirements-dev.in
#
astroid==2.0.4 # via pylint
beautifulsoup4==4.6.3 # via bs4
bs4==0.0.1
certifi==2018.10.15 # via requests
chardet==3.0.4 # via requests
click==7.0 # via flask
flask==1.0.2
idna==2.7 # via requests
isort==4.3.4 # via pylint
itsdangerous==1.1.0 # via flask
jinja2==2.10 # via flask
lazy-object-proxy==1.3.1 # via astroid
markupsafe==1.1.0 # via jinja2
mccabe==0.6.1 # via pylint
peewee==3.7.1
pylint==2.1.1
requests==2.20.1
six==1.11.0 # via astroid
urllib3==1.24.1 # via requests
werkzeug==0.14.1 # via flask
wrapt==1.10.11 # via astroid

4
requirements.in Normal file
View File

@@ -0,0 +1,4 @@
flask
peewee
bs4
requests

View File

@@ -1,7 +1,20 @@
flask
peewee
flask-peewee
bs4
more_itertools
requests
utilkit
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements.txt requirements.in
#
beautifulsoup4==4.6.3 # via bs4
bs4==0.0.1
certifi==2018.10.15 # via requests
chardet==3.0.4 # via requests
click==7.0 # via flask
flask==1.0.2
idna==2.7 # via requests
itsdangerous==1.1.0 # via flask
jinja2==2.10 # via flask
markupsafe==1.1.0 # via jinja2
peewee==3.7.1
requests==2.20.1
urllib3==1.24.1 # via requests
werkzeug==0.14.1 # via flask

View File

@@ -7,18 +7,18 @@ https://github.com/pypa/sampleproject
from setuptools import setup
# To use a consistent encoding
from codecs import open
from codecs import open as codecopen
from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the relevant file
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
with codecopen(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()
setup(
name='digimarks', # pip install digimarks
description='Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching.',
description='Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic title fetching and REST API calls.',
#long_description=open('README.md', 'rt').read(),
long_description=long_description,
@@ -26,7 +26,7 @@ setup(
# third part for minor release
# second when api changes
# first when it becomes stable someday
version='1.0.0',
version='1.1.99',
author='Michiel Scholten',
author_email='michiel@diginaut.net',
@@ -35,7 +35,7 @@ setup(
# as a practice no need to hard code version unless you know program wont
# work unless the specific versions are used
install_requires=['Flask', 'Peewee', 'Flask-Peewee', 'requests', 'utilkit'],
install_requires=['Flask', 'Peewee', 'Flask-Peewee', 'requests', 'bs4'],
py_modules=['digimarks'],

View File

@@ -1,77 +0,0 @@
/* label color */
.input-field label {
color: #000;
}
/* label focus color */
.input-field input[type=text]:focus + label {
color: #000;
}
/* label underline focus color */
.input-field input[type=text]:focus {
border-bottom: 1px solid #000;
box-shadow: 0 1px 0 0 #000;
}
/* valid color */
.input-field input[type=text].valid {
border-bottom: 1px solid #000;
box-shadow: 0 1px 0 0 #000;
}
/* invalid color */
.input-field input[type=text].invalid {
border-bottom: 1px solid #000;
box-shadow: 0 1px 0 0 #000;
}
/* icon prefix focus color */
.input-field .prefix.active {
color: #000;
}
/* Card title anchor colour */
.white-text .card-title a,
.white-text a
{
color: #FFF;
}
.chip a,
.white-text .chip a
{
color: #1b5e20; /* green darken-4 */
}
.card.tiny
{
height: 140px;
overflow: hidden;
}
.card.tiny .card-title
{
font-size: 18px;
}
.card .card-reveal .digimark-card-header,
.card .digimark-card-header.activator,
.chip.clickable
{
cursor: pointer;
/*display: block;*/
}
.card .digimark-card-content
{
padding-top: 10px;
}
.card-image i
{
padding: 5px 0 0 15px;
}
.card.horizontal .card-image img.favicon
{
height: 60px;
width: 60px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -1,7 +0,0 @@
(function($){
$(function(){
$('.button-collapse').sideNav();
}); // end of document ready
})(jQuery); // end of jQuery name space

View File

@@ -1,6 +0,0 @@
{% extends "base.html" %}
{% block title %}404: Page not found{% endblock %}
{% block pageheader %}404: Page not found{% endblock %}
{% block pagecontent %}
The page you requested was not found.
{% endblock %}

View File

@@ -1,63 +0,0 @@
<!doctype html>
<html>
<head>
<title>{% block title %}{% endblock %} - digimarks</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"/>
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#2e7d32" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#2e7d32">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Roboto+Mono&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>
<link href="{{ url_for('static', filename='css/digimarks.css') }}" type="text/css" rel="stylesheet" media="screen,projection"/>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
</head>
<body class="grey lighten-4">
<nav class="green darken-3" role="navigation">
<div class="nav-wrapper container"><a id="logo-container" href="{% if userkey %}{{ url_for('bookmarks', userkey=userkey) }}{% else %}{{ url_for('index') }}{% endif %}" class="brand-logo">digimarks</a>
<ul class="right hide-on-med-and-down">
{% if userkey %}
<li><a href="{{ url_for('tags', userkey=userkey) }}">Tags</a></li>
<li><a href="{{ url_for('addbookmark', userkey=userkey) }}">Add bookmark</a></li>
{% endif %}
</ul>
{% if userkey %}
<ul id="nav-mobile" class="side-nav">
<li><a class="waves-effect" href="{{ url_for('bookmarks', userkey=userkey) }}"><i class="material-icons">turned_in</i>Home</a></li>
<li><a class="waves-effect" href="{{ url_for('tags', userkey=userkey) }}"><i class="material-icons">label</i>Tags</a></li>
<li><a class="waves-effect" href="{{ url_for('addbookmark', userkey=userkey) }}"><i class="material-icons">add</i>Add bookmark</a></li>
</ul>
<a href="#" data-activates="nav-mobile" class="button-collapse"><i class="material-icons">menu</i></a>
{% endif %}
</div>
</nav>
<div class="section no-pad-bot" id="index-banner">
<div class="container">
<div class="header grey-text lighten-5">
<h1>{% block pageheader %}Bookmarks{% endblock %}</h1>
</div>
</div>
</div>
<div class="container">
<div class="section">
{% block pagecontent %}
{% endblock %}
</div>
</div>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>
<script src="{{ url_for('static', filename='js/init.js') }}"></script>
</body>
</html>

View File

@@ -1,146 +0,0 @@
{% extends "base.html" %}
{% if not action %}
{% set action = 'Bookmarks' %}
{% endif %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if tag and not publictag %}
<div class="row">
<div class="col s12">
<a href="{{ url_for('addpublictag', userkey=userkey, tag=tag) }}">Create public page <i class="material-icons right">tag</i></a>
</div>
</div>
{% endif %}
{% if tag and publictag %}
<div class="row">
<div class="col s12"><a href="{{ url_for('publictag', tagkey=publictag.tagkey) }}">Public link</a></div>
</div>
{% endif %}
{% if message %}
<div class="row">
<div class="col s12">
<div class="card-panel orange lighten-2">
<span class="white-text">
{{ message|safe }}
</span>
</div>
</div>
</div>
{% endif %}
<div class="row">
<form action="{{ url_for('bookmarks', userkey=userkey) }}" method="POST">
<div class="input-field col l10 m10 s8">
<input placeholder="search text" type="text" name="filter_text" id="filter_text" value="{{ filter_text }}" class="validate" />
</div>
<div class="input-field col l2 m2 s4">
<p class="left-align"><button class="btn waves-effect waves-light" type="submit" name="submit">Filter</button></p>
</div>
</form>
</div>
{% if tags %}
<div class="row">
<div class="col s12">
<ul class="collapsible" data-collapsible="expandable">
<li>
<div class="collapsible-header"><i class="material-icons">label</i>Filter on star/problem/comment/tag</div>
<div class="collapsible-body" style="padding: 10px;">
<div class="chip">
<a href="{{ url_for('bookmarks', userkey=userkey, filtermethod='starred') }}"><i class="tiny material-icons yellow-text">star</i></a>
</div>
<div class="chip">
<a href="{{ url_for('bookmarks', userkey=userkey, filtermethod='broken') }}"><i class="tiny material-icons red-text">report_problem</i></a>
</div>
<div class="chip">
<a href="{{ url_for('bookmarks', userkey=userkey, filtermethod='note') }}"><i class="tiny material-icons">comment</i></a>
</div>
{% for tag in tags %}
<div class="chip">
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
</li>
</ul>
</div>
</div>
{% endif %}
<div class="row">
{% for bookmark in bookmarks %}
<div class="col s12 m6 l4">
{#
<div class="thumbnail">
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}">
<img style="width:450px;" src="{{ bookmark.image }}" />
</a>
<p><a href="{{ bookmark.url }}">{{ bookmark.url|urlize(25) }}</a></p>
<p>{{ bookmark.created_date.strftime("%m/%d/%Y %H:%M") }}</p>
</div>
#}
<div class="card horizontal tiny green darken-3">
<div class="card-image">
{% if bookmark.favicon %}
<div><img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" class="favicon" /></div>
{% endif %}
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
<div><i class="small material-icons red-text" title="HTTP status {{ bookmark.http_status }}">report_problem</i></div>
{% endif %}
{% if bookmark.starred == True %}
<div><i class="small material-icons yellow-text">star</i></div>
{% endif %}
{% if bookmark.note %}
<div><i class="small material-icons white-text" title="{{ bookmark.note|truncate(100) }}">comment</i></div>
{% endif %}
</div>
<div class="card-stacked">
<div class="card-content white-text">
<span class="digimark-card-header activator">
{% for tag in bookmark.tags_list %}
<div class="chip">
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
<i class="material-icons right">more_vert</i>
</span>
<div class="digimark-card-content">
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}" rel="noreferrer noopener" target="_blank">
{% if bookmark.title %}
{{ bookmark.title }}
{% else %}
{{ bookmark.get_uri_domain() }} (no title)
{% endif %}
</a>
</div>
</div>
</div>
<div class="card-reveal green darken-3">
<span class="card-title white-text">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
<div class="white-text" style="padding-top: 10px;">
<a href="{{ url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" style="padding: 3px"><i class="tiny material-icons">mode_edit</i> EDIT</a>
<a href="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" style="padding: 3px" class="red-text"><i class="tiny material-icons">delete</i> DELETE</a>
</div>
</div>
</div>
</div>
{% endfor %}
{#
<div class="pagination">
{% if page > 1 %}<a href="./?page={{ page - 1 }}">Previous</a>{% endif %}
{% if pagination.get_pages() > page %}<a href="./?page={{ page + 1 }}">Next</a>{% endif %}
</div>
#}
</div>
<div class="fixed-action-btn" style="bottom: 20px; right: 20px;">
<a class="btn-floating btn-large red" href="{{ url_for('addbookmark', userkey=userkey) }}">
<i class="large material-icons">add</i>
</a>
</div>
{% endblock %}

View File

@@ -1,172 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
<div class="row">
<div class="col s12">
<div class="card-panel red darken-1">
<span class="white-text">
{% if bookmark.http_status == 404 %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;URL not found (404), broken/outdated link?
{% elif bookmark.http_status == 302 %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;HTTP status (302), moved temporarily. Use button for new target
{% elif bookmark.http_status == bookmark.HTTP_CONNECTIONERROR %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;Connection error, server might have been offline at the time of last edit
{% else %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;HTTP status {{ bookmark.http_status }}
{% endif %}
</span>
</div>
</div>
</div>
{% endif %}
{% if message %}
<div class="row">
<div class="col s12">
<div class="card-panel orange lighten-2">
<span class="white-text">
{{ message }}
</span>
</div>
</div>
</div>
{% endif %}
{% if formaction and formaction == 'edit' %}
<form class="digimark" action="{{ url_for('editingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST">
{% else %}
<form class="digimark" action="{{ url_for('addingbookmark', userkey=userkey) }}" method="POST">
{% endif %}
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">description</i>
<input placeholder="title (leave empty for autofetch)" type="text" name="title" id="title" value="{{ bookmark.title }}" class="validate" />
<label for="title">Title</label>
</div>
<div class="input-field col s12">
<i class="material-icons prefix">turned_in</i>
<input placeholder="url" type="text" name="url" id="url" value="{{ bookmark.url }}" class="validate" />
<label for="url">URL</label>
{% if bookmark.get_redirect_uri() %}
<div>
<a class="waves-effect waves-light btn" id="btn_urlupdate"><i class="material-icons left">turned_in</i>{{ bookmark.get_redirect_uri() }}</a>
</div>
<script type="text/javascript">
$(function () {
$('#btn_urlupdate').on('click', function () {
var text = $('#url');
text.val('{{ bookmark.get_redirect_uri() }}');
});
});
</script>
{% endif %}
</div>
<div class="input-field col s12">
<i class="material-icons prefix">comment</i>
<input placeholder="note" type="text" name="note" id="note" value="{{ bookmark.note }}" class="validate" />
<label for="note">Note</label>
</div>
<div class="input-field col s12">
<i class="material-icons prefix">label</i>
<input placeholder="tags, divided by comma's" type="text" name="tags" id="tags" value="{{ bookmark.tags }}" class="validate" />
<label for="tags">Tags</label>
</div>
</div>
{% if tags %}
<div class="row">
<div class="col s12">
<ul class="collapsible" data-collapsible="expandable">
<li>
<div class="collapsible-header"><i class="material-icons">label</i>Existing tags</div>
<div class="collapsible-body" style="padding: 10px;">
{% for tag in tags %}
<div class="chip clickable" id="tag_{{ tag }}">
{{ tag }}
</div>
{% endfor %}
</div>
</li>
</ul>
</div>
{% for tag in tags %}
<script type="text/javascript">
$(function () {
$('#tag_{{ tag }}').on('click', function () {
var text = $('#tags');
text.val(text.val() + ', {{ tag }}');
});
});
</script>
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="input-field col s12">
{#<i class="material-icons prefix">star</i>#}
<input type="checkbox" name="starred" id="starred" {% if bookmark.starred == True %}checked{% endif %} />
<label for="starred">Starred</label>
</div>
<div class="input-field col s12">
<input type="checkbox" name="strip" id="strip" />
<label for="strip">Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</label>
</div>
{% if bookmark.url_hash %}
</div>
<div class="row">
<div class="col l4 m6 s12">
<table>
<tr>
<th>Added</th>
<td>{{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% if bookmark.modified_date %}
<tr>
<th>Modified</th>
<td>{{ bookmark.modified_date.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endif %}
{% if bookmark.deleted_date %}
<tr>
<th>Deleted</th>
<td>{{ bookmark.deleted_date.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="row">
{% endif %}
<div class="input-field col l2 m3 s4">
<p class="left-align"><button class="btn waves-effect waves-light" type="submit" name="submit" id="submit">Save <i class="material-icons right">send</i></button></p>
</div>
{% if bookmark.url_hash %}
</form>
<div class="input-field col l2 m3 s4">
<form action="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST">
<p class="left-align"><button class="btn waves-effect waves-light" type="submit" name="delete">Delete <i class="material-icons right">delete</i></button></p>
</form>
</div>
</div>
{% else %}
</div>
</form>
{% endif %}
<script>
$(function() {
console.log('woei');
$('form.digimark').on('submit',function(){$("#submit").prop("disabled", true); return true;})
});
</script>
{% endblock %}

View File

@@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block title %}digimarks{% endblock %}
{% block pageheader %}digimarks{% endblock %}
{% block pagecontent %}
<p>Please visit your personal url, or <a href="https://github.com/aquatix/digimarks">see the digimarks project page</a>.</p>
<div class="row">
<div class="col s12">
<div class="card-panel orange lighten-2">
<span class="black-text">
If you forgot/lost your personal url, contact your digimarks administrator. On startup, the personal codes are printed to the standard output (so should be findable in a log). Of course, bookmarks.db contains the user information too.
</span>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,68 +0,0 @@
{% extends "base.html" %}
{% if not action %}
{% set action = 'Bookmarks' %}
{% endif %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if message %}
<div class="row">
<div class="col s12">
<div class="card-panel orange lighten-2">
<span class="white-text">
{{ message }}
</span>
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col s12">
<a href="{{ url_for('publictagfeed', tagkey=tagkey) }}"><i class="material-icons tiny">rss_feed</i> feed</a>
</div>
</div>
<div class="row">
{% for bookmark in bookmarks %}
<div class="col s12 m6 l4">
<div class="card horizontal tiny green darken-3">
<div class="card-image">
{% if bookmark.favicon %}
<div><img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" class="favicon" /></div>
{% endif %}
{% if bookmark.http_status != 200 %}
<i class="small material-icons red-text" title="HTTP status {{ bookmark.http_status }}">report_problem</i><br />
{% endif %}
{% if bookmark.starred == True %}
<i class="small material-icons yellow-text">star</i>
{% endif %}
{% if bookmark.note %}
<div><i class="small material-icons white-text" title="{{ bookmark.note|truncate(100) }}">comment</i></div>
{% endif %}
</div>
<div class="card-stacked">
<div class="card-content white-text">
<span class="digimark-card-header activator">
<i class="material-icons right">more_vert</i>
</span>
<div class="digimark-card-content">
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}" rel="noreferrer noopener" target="_blank">
{% if bookmark.title %}
{{ bookmark.title }}
{% else %}
{{ bookmark.get_uri_domain() }} (no title)
{% endif %}
</a>
</div>
</div>
</div>
<div class="card-reveal green darken-3">
<span class="card-title white-text">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,65 +0,0 @@
{% extends "base.html" %}
{% block title %}Tags{% endblock %}
{% block pageheader %}Tags{% endblock %}
{% block pagecontent %}
<div class="row">
<div class="col s12">
<table class="centered">
<thead>
<tr>
<th><i class="material-icons" title="Unique labels">label</i></th>
<th><i class="material-icons green-text" title="Public tag pages">present_to_all</i></th>
<th><i class="material-icons" title="Total bookmarks">turned_in</i></th>
<th><i class="material-icons" title="Bookmarks with notes">comment</i></th>
<th><i class="material-icons yellow-text" title="Starred bookmarks">star</i></th>
<th><i class="material-icons orange-text" title="HTTP status is not 200 OK">warning</i></th>
<th><i class="material-icons red-text" title="Deleted bookmarks">delete</i></th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ totaltags }}</td>
<td>{{ totalpublic }}</td>
<td>{{ totalbookmarks }}</td>
<td>{{ totalnotes }}</td>
<td>{{ totalstarred }}</td>
<td>{{ totalhttperrorstatus }}</td>
<td>{{ totaldeleted }}</td>
</tr>
</tbody>
</table>
<br /><br />
<table>
<thead>
<tr>
<th>Tag</th>
<th>Public link</th>
<th>Number of bookmarks</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr>
<td>
<a href="{{ url_for('tag', userkey=userkey, tag=tag['tag']) }}">{{ tag['tag'] }}</a>
</td>
<td>
{% if tag['publictag'] %}
<a href="{{ url_for('publictag', tagkey=tag['publictag'].tagkey) }}">Public link</a> (<a href="{{ url_for('removepublictag', tag=tag['tag'], tagkey=tag['publictag'].tagkey, userkey=userkey) }}">Delete</a> <i class="tiny material-icons red-text">warning</i>)
{% else %}
<a href="{{ url_for('addpublictag', userkey=userkey, tag=tag['tag']) }}">Create</a>
{% endif %}
</td>
<td>
{{ tag['total'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

10
wsgi.py
View File

@@ -1,7 +1,13 @@
# Activate virtualenv
import settings
activate_this = settings.VENV
execfile(activate_this, dict(__file__=activate_this))
activate_this = getattr(settings, 'VENV', None)
# FIXME: python 2 *and* python 3 compatibility
# Python 2
#if activate_this:
# execfile(activate_this, dict(__file__=activate_this))
# Python 3
with open(activate_this) as file_:
exec(file_.read(), dict(__file__=activate_this))
from digimarks import app as application