mirror of
https://github.com/aquatix/digimarks.git
synced 2025-12-06 23:05:10 +01:00
Compare commits
193 Commits
v1.1.0
...
snyk-fix-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9941513c08 | ||
| ec002e159b | |||
| d3dff5a4e8 | |||
| b6ffc24175 | |||
| c5ca4d97e3 | |||
| 372c382364 | |||
|
|
6ff63f47fe | ||
| 3fbab07944 | |||
| db091ae02e | |||
| 24935dad9f | |||
| 6864e7f5a4 | |||
| ed8e02f0d5 | |||
| 76e4924e2c | |||
| a35fbffaec | |||
| fad7dc59bc | |||
| 1a4ca1a4c7 | |||
|
|
ad614b2872 | ||
| b658651c07 | |||
| c1d795e704 | |||
| 4b36b448cf | |||
|
|
29b1d045cd | ||
| d8bf52c9d2 | |||
| 010905086f | |||
| a4225829e3 | |||
| f3dff354fc | |||
| ca71fa66df | |||
| 09ab5acf76 | |||
| e90d35238a | |||
| 3092f83c8b | |||
| a27787e956 | |||
| 21800911db | |||
| 20a3d9f838 | |||
| 0ab3bd2263 | |||
| 32b074b859 | |||
| 5789bbe006 | |||
| 2ef7358ac7 | |||
| d1e590390c | |||
| 7a1bc11004 | |||
| 315c664fcc | |||
| db5944cec4 | |||
|
|
becb734d17 | ||
| 64ee0856c5 | |||
|
|
6c2be3070e | ||
| 426c2eda68 | |||
| b0e53d4a85 | |||
| 1f69d9e53f | |||
|
|
fc27d9f186 | ||
|
|
6341b384bf | ||
| f698ebfe18 | |||
| 9f736ffe82 | |||
| e1a45a21b5 | |||
| 9492d26511 | |||
| f7762ebc7b | |||
| 1c4bc61494 | |||
| 2a87e0aa1f | |||
| 0a24c7d170 | |||
| 2615089acd | |||
| 14bf22f3e5 | |||
| e55fb7bd5f | |||
| 4e1261857d | |||
| f9861c1491 | |||
| 836077ad12 | |||
| 0913ffca2c | |||
| 834f95c34b | |||
| c82e3a02d4 | |||
| 7874002fef | |||
| 6db0355cc7 | |||
|
|
0495296f0f | ||
|
|
a5d225fb56 | ||
| f6401a3e9f | |||
| 46e7fc9899 | |||
| f6befd0700 | |||
| 95ff9c01ca | |||
| b36cd8db6b | |||
| 2b19b770dc | |||
| 6dc74c7102 | |||
| ac7906d781 | |||
| 5d2329ff90 | |||
| a29e14b7a7 | |||
| 5e4a35527b | |||
| 55609aa353 | |||
| 0d86c2608a | |||
| dd1e3a19ff | |||
| 9694ca566b | |||
| 17caef1aed | |||
| 52a01794f6 | |||
| 8148a79d28 | |||
| d6e74ff328 | |||
| 540fd6ba91 | |||
| 3becf27b42 | |||
| fe990ecf63 | |||
| 75080579fd | |||
| 94eb42a882 | |||
| d501c6b4db | |||
| 11e159db8d | |||
| 6516c4af1d | |||
| 63636d3355 | |||
| 52cc93d4c3 | |||
| 6f9d44ce86 | |||
| 718b39a267 | |||
| bfc4fb702a | |||
| 7e2f2f6f6e | |||
| 29c8c875be | |||
| 0f0caed748 | |||
| 971590196e | |||
| 3653b5e424 | |||
| 330523ba3f | |||
| 199b641a38 | |||
| c0c8e35246 | |||
| 6de9ba2642 | |||
| e48f2c98c3 | |||
| 554f651ec8 | |||
| 6def8d60a5 | |||
| 1d531989bb | |||
| fc2712f5e3 | |||
| 76ef520815 | |||
| ea4a7bdcd7 | |||
| aee0515eae | |||
| 3835497918 | |||
| d7b2c28c96 | |||
| fac3a4f747 | |||
| 0548f35b39 | |||
| 8372d6e2a5 | |||
| cd2911e7f0 | |||
| 127d99b1e0 | |||
| bb4f81262e | |||
| 3c697d3162 | |||
| fefb317ddf | |||
| 37ebdda933 | |||
| 3516fbfbb2 | |||
| c4f921ac68 | |||
| fec54c51f7 | |||
| 9f467f8a09 | |||
| cac31e40c9 | |||
| 1e14163d42 | |||
| 88eee28b88 | |||
| 45c95f5f17 | |||
| 9539c48b7b | |||
| 46aa230fae | |||
| c6089f1caa | |||
| 45d44d3bdf | |||
| e4662351c2 | |||
| 62f3ddf654 | |||
| 072ec6c426 | |||
| 5055947351 | |||
| 5f131b15ef | |||
| bd808a9e1d | |||
| 3b019e4368 | |||
| 3af1239326 | |||
| e8ea948566 | |||
| 1c2090f300 | |||
| cdfd0341f0 | |||
| 01d6525861 | |||
| c70e53a658 | |||
| dc76a592e0 | |||
| a4aae2d6c4 | |||
| 389e63bdbb | |||
| 6ba4803ed2 | |||
| 71756f9ea0 | |||
| 67635c199a | |||
| 7b2a861652 | |||
| 53887c8ece | |||
| 2cc2d382c1 | |||
| fa033452f1 | |||
| c14e24430b | |||
| cca4504fd7 | |||
| 7f6dc3f3df | |||
| 4170a7818b | |||
|
|
48e77f551d | ||
| e35d9952bd | |||
| 6933357a61 | |||
| 9578ee624b | |||
| ded047d749 | |||
| 57226b88f5 | |||
| de5d4d30ef | |||
| 60f5a48d89 | |||
| 47f6e36e4b | |||
| 5402dfc320 | |||
| 90f1322c48 | |||
| d1aef0284f | |||
| d7a5bd921f | |||
| b85ee43cc7 | |||
| 8077499eae | |||
| ab06f7e583 | |||
| f309d4acf2 | |||
| 88a9806d44 | |||
| 6f4d270858 | |||
| c354613b60 | |||
| 383c77ee8b | |||
| 0e77afd000 | |||
| f617fb8190 | |||
| 34af0e9ab7 | |||
| 7f866658e3 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -87,3 +87,13 @@ ENV/
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# vim
|
||||
*.swp
|
||||
|
||||
# digimarks
|
||||
static/favicons
|
||||
tags
|
||||
*.db_*
|
||||
*.db
|
||||
settings.py
|
||||
|
||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,8 +1,8 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
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
|
||||
@@ -11,14 +11,58 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
- 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
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 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
|
||||
|
||||
@@ -27,6 +71,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
- 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -39,6 +39,8 @@ Usage / example configuration
|
||||
Copy ``settings.py`` from example_config to the parent directory and
|
||||
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
|
||||
|
||||
Do not forget to fill in the `MASHAPE_API_KEY` value, which you [can request on the RapidAPI website](https://rapidapi.com/realfavicongenerator/api/realfavicongenerator).
|
||||
|
||||
Run digimarks as a service under nginx or apache and call the appropriate
|
||||
url's when wanted.
|
||||
|
||||
@@ -92,13 +94,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
|
||||
|
||||
0
__init__.py
Normal file
0
__init__.py
Normal file
607
digimarks.py
607
digimarks.py
@@ -1,23 +1,35 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import binascii
|
||||
import datetime
|
||||
import gzip
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import bs4
|
||||
from urlparse import urlparse, urlunparse, urljoin
|
||||
import requests
|
||||
from dateutil import tz
|
||||
from feedgen.feed import FeedGenerator
|
||||
from flask import (Flask, abort, jsonify, make_response, redirect,
|
||||
render_template, request, url_for)
|
||||
from peewee import * # noqa
|
||||
|
||||
from flask import Flask, abort, redirect, render_template, request, url_for, jsonify
|
||||
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
|
||||
try:
|
||||
# Python 3
|
||||
from urllib.parse import urljoin, urlparse, urlunparse
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from urlparse import urljoin, urlparse, urlunparse
|
||||
|
||||
DEFAULT_THEME = 'green'
|
||||
|
||||
DIGIMARKS_USER_AGENT = 'digimarks/1.2.0-dev'
|
||||
|
||||
DEFAULT_THEME = 'freshgreen'
|
||||
themes = {
|
||||
'green': {
|
||||
'BROWSERCHROME': '#2e7d32', # green darken-2
|
||||
'BODY': 'grey lighten-4',
|
||||
'TEXT': 'black-text',
|
||||
'TEXTHEX': '#000',
|
||||
@@ -27,8 +39,13 @@ themes = {
|
||||
'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',
|
||||
@@ -36,6 +53,7 @@ themes = {
|
||||
'COMMENT': '',
|
||||
},
|
||||
'freshgreen': {
|
||||
'BROWSERCHROME': '#43a047', # green darken-1
|
||||
'BODY': 'grey lighten-5',
|
||||
'TEXT': 'black-text',
|
||||
'TEXTHEX': '#000',
|
||||
@@ -45,15 +63,45 @@ themes = {
|
||||
'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',
|
||||
@@ -63,8 +111,37 @@ themes = {
|
||||
'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',
|
||||
@@ -93,7 +170,11 @@ DATABASE = {
|
||||
# create our flask app and a database wrapper
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
db = Database(app)
|
||||
database = SqliteDatabase(os.path.join(APP_ROOT, 'bookmarks.db'))
|
||||
|
||||
# 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:
|
||||
@@ -103,7 +184,7 @@ except AttributeError:
|
||||
|
||||
# Cache the tags
|
||||
all_tags = {}
|
||||
settings = {}
|
||||
usersettings = {}
|
||||
|
||||
|
||||
def ifilterfalse(predicate, iterable):
|
||||
@@ -142,15 +223,15 @@ def clean_tags(tags_list):
|
||||
|
||||
|
||||
magic_dict = {
|
||||
"\x1f\x8b\x08": "gz",
|
||||
"\x42\x5a\x68": "bz2",
|
||||
"\x50\x4b\x03\x04": "zip"
|
||||
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) as f:
|
||||
with open(filename, "rb") as f:
|
||||
file_start = f.read(max_len)
|
||||
for magic, filetype in magic_dict.items():
|
||||
if file_start.startswith(magic):
|
||||
@@ -158,7 +239,12 @@ def file_type(filename):
|
||||
return "no match"
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
""" User account """
|
||||
username = CharField()
|
||||
key = CharField()
|
||||
@@ -167,11 +253,11 @@ class User(db.Model):
|
||||
|
||||
def generate_key(self):
|
||||
""" Generate userkey """
|
||||
self.key = os.urandom(24).encode('hex')
|
||||
self.key = binascii.hexlify(os.urandom(24))
|
||||
return self.key
|
||||
|
||||
|
||||
class Bookmark(db.Model):
|
||||
class Bookmark(BaseModel):
|
||||
""" Bookmark instance, connected to User """
|
||||
# Foreign key to User
|
||||
userkey = CharField()
|
||||
@@ -190,6 +276,7 @@ class Bookmark(db.Model):
|
||||
# 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
|
||||
|
||||
@@ -209,30 +296,19 @@ class Bookmark(db.Model):
|
||||
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()
|
||||
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)
|
||||
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:
|
||||
if self.http_status == 200 or self.http_status == 202:
|
||||
html = bs4.BeautifulSoup(result.text, 'html.parser')
|
||||
try:
|
||||
self.title = html.title.text.strip()
|
||||
@@ -243,24 +319,27 @@ class Bookmark(db.Model):
|
||||
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)
|
||||
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(self):
|
||||
def _set_favicon_with_iconsbetterideaorg(self, domain):
|
||||
""" 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)
|
||||
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)
|
||||
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)
|
||||
@@ -272,14 +351,70 @@ class Bookmark(db.Model):
|
||||
origcontent = orig.read()
|
||||
orig.close()
|
||||
os.remove(filename)
|
||||
new = file(filename, 'wb')
|
||||
new.write(origcontent)
|
||||
new.close()
|
||||
with open(filename, 'wb') as new:
|
||||
new.write(origcontent)
|
||||
self.favicon = domain + fileextension
|
||||
|
||||
def set_tags(self, tags):
|
||||
def _set_favicon_with_realfavicongenerator(self, domain):
|
||||
""" Fetch favicon for the domain """
|
||||
response = requests.get(
|
||||
'https://realfavicongenerator.p.rapidapi.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.rapidapi.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 = tags.split(',')
|
||||
tags_split = newtags.split(',')
|
||||
tags_clean = clean_tags(tags_split)
|
||||
self.tags = ','.join(tags_clean)
|
||||
|
||||
@@ -287,12 +422,11 @@ class Bookmark(db.Model):
|
||||
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)
|
||||
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
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_uri_domain(self):
|
||||
parsed = urlparse(self.url)
|
||||
@@ -308,22 +442,24 @@ class Bookmark(db.Model):
|
||||
""" Get the tags as a list, iterable in template """
|
||||
if self.tags:
|
||||
return self.tags.split(',')
|
||||
else:
|
||||
return []
|
||||
|
||||
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,
|
||||
}
|
||||
'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,
|
||||
}
|
||||
return result
|
||||
|
||||
@property
|
||||
def serialize(self):
|
||||
return self.to_dict()
|
||||
|
||||
class PublicTag(db.Model):
|
||||
|
||||
class PublicTag(BaseModel):
|
||||
""" Publicly shared tag """
|
||||
tagkey = CharField()
|
||||
userkey = CharField()
|
||||
@@ -332,7 +468,7 @@ class PublicTag(db.Model):
|
||||
|
||||
def generate_key(self):
|
||||
""" Generate hash-based key for publicly shared tag """
|
||||
self.tagkey = os.urandom(16).encode('hex')
|
||||
self.tagkey = binascii.hexlify(os.urandom(16))
|
||||
|
||||
|
||||
def get_tags_for_user(userkey):
|
||||
@@ -354,7 +490,7 @@ def get_cached_tags(userkey):
|
||||
|
||||
def get_theme(userkey):
|
||||
try:
|
||||
usertheme = settings[userkey]['theme']
|
||||
usertheme = usersettings[userkey]['theme']
|
||||
return themes[usertheme]
|
||||
except KeyError:
|
||||
return themes[DEFAULT_THEME] # default
|
||||
@@ -364,9 +500,22 @@ def make_external(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('/')
|
||||
@@ -376,10 +525,7 @@ def index():
|
||||
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)
|
||||
@@ -389,7 +535,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:
|
||||
@@ -408,11 +554,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())
|
||||
@@ -420,25 +565,109 @@ 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 bookmarks, bookmarktags, filter_text, message
|
||||
|
||||
|
||||
@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=tags, filter_text=filter_text, message=message, theme=theme)
|
||||
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>')
|
||||
#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>/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())
|
||||
resp = make_response(render_template(
|
||||
'bookmarks.js',
|
||||
bookmarks=bookmarks
|
||||
))
|
||||
resp.headers['Content-type'] = 'text/javascript; charset=utf-8'
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/<userkey>/<urlhash>/json')
|
||||
def viewbookmarkjson(userkey, urlhash):
|
||||
@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>')
|
||||
@@ -456,7 +685,16 @@ def editbookmark(userkey, urlhash):
|
||||
# Workaround for when an existing bookmark has a null note
|
||||
bookmark.note = ''
|
||||
theme = get_theme(userkey)
|
||||
return render_template('edit.html', action='Edit bookmark', userkey=userkey, bookmark=bookmark, message=message, formaction='edit', tags=tags, theme=theme)
|
||||
return render_template(
|
||||
'edit.html',
|
||||
action='Edit bookmark',
|
||||
userkey=userkey,
|
||||
bookmark=bookmark,
|
||||
message=message,
|
||||
formaction='edit',
|
||||
tags=tags,
|
||||
theme=theme
|
||||
)
|
||||
|
||||
|
||||
@app.route('/<userkey>/add')
|
||||
@@ -471,10 +709,18 @@ def addbookmark(userkey):
|
||||
message = request.args.get('message')
|
||||
tags = get_cached_tags(userkey)
|
||||
theme = get_theme(userkey)
|
||||
return render_template('edit.html', action='Add bookmark', userkey=userkey, bookmark=bookmark, tags=tags, message=message, theme=theme)
|
||||
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')
|
||||
@@ -517,7 +763,7 @@ def updatebookmark(userkey, request, urlhash = None):
|
||||
else:
|
||||
bookmark.set_status_code()
|
||||
|
||||
if bookmark.http_status == 200:
|
||||
if bookmark.http_status == 200 or bookmark.http_status == 202:
|
||||
try:
|
||||
bookmark.set_favicon()
|
||||
except IOError:
|
||||
@@ -535,7 +781,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':
|
||||
@@ -550,7 +796,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))
|
||||
@@ -559,27 +805,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)
|
||||
@@ -590,25 +843,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()
|
||||
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)
|
||||
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')
|
||||
@@ -619,30 +890,62 @@ def tag(userkey, tag):
|
||||
publictag = None
|
||||
|
||||
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)
|
||||
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())
|
||||
this_tag, bookmarks = get_publictag(tagkey)
|
||||
theme = themes[DEFAULT_THEME]
|
||||
return render_template('publicbookmarks.html', bookmarks=bookmarks, tag=tag, action=this_tag.tag, tagkey=tagkey, theme=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)
|
||||
@@ -651,23 +954,43 @@ 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 = FeedGenerator()
|
||||
feed.title(this_tag.tag)
|
||||
feed.id(request.url)
|
||||
feed.link(href=request.url, rel='self')
|
||||
feed.link(href=make_external(url_for('publictag_page', tagkey=tagkey)))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
entry = feed.add_entry()
|
||||
|
||||
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)
|
||||
return feed.get_response()
|
||||
bookmarktitle = '{} (no title)'.format(bookmark.url)
|
||||
if bookmark.title:
|
||||
bookmarktitle = bookmark.title
|
||||
|
||||
entry.id(bookmark.url)
|
||||
entry.title(bookmarktitle)
|
||||
entry.link(href=bookmark.url)
|
||||
entry.author(name='digimarks')
|
||||
entry.pubdate(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
|
||||
entry.published(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
|
||||
entry.updated(updated_date.replace(tzinfo=tz.tzlocal()))
|
||||
|
||||
response = make_response(feed.atom_str(pretty=True))
|
||||
response.headers.set('Content-Type', 'application/atom+xml')
|
||||
return response
|
||||
except PublicTag.DoesNotExist:
|
||||
abort(404)
|
||||
|
||||
@@ -691,10 +1014,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'])
|
||||
@@ -702,7 +1025,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')
|
||||
@@ -714,7 +1037,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)
|
||||
|
||||
@@ -737,6 +1060,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)
|
||||
@@ -746,10 +1091,10 @@ users = User.select()
|
||||
print('Current user keys:')
|
||||
for user in users:
|
||||
all_tags[user.key] = get_tags_for_user(user.key)
|
||||
settings[user.key] = {'theme': user.theme}
|
||||
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)
|
||||
|
||||
@@ -10,6 +10,10 @@ DEBUG = False
|
||||
# echo -n "yourstring" | sha1sum
|
||||
SYSTEMKEY = 'S3kr1t'
|
||||
|
||||
# RapidAPI key for favicons
|
||||
# https://rapidapi.com/realfavicongenerator/api/realfavicongenerator
|
||||
MASHAPE_API_KEY = 'your_MASHAPE_key'
|
||||
|
||||
LOG_LOCATION = 'digimarks.log'
|
||||
#LOG_LOCATION = '/var/log/digimarks/digimarks.log'
|
||||
# How many logs to keep in log rotation:
|
||||
|
||||
3
requirements-dev.in
Normal file
3
requirements-dev.in
Normal file
@@ -0,0 +1,3 @@
|
||||
-r requirements.in
|
||||
|
||||
pylint
|
||||
61
requirements-dev.txt
Normal file
61
requirements-dev.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements-dev.in
|
||||
astroid==3.3.11
|
||||
# via pylint
|
||||
beautifulsoup4==4.13.5
|
||||
# via bs4
|
||||
blinker==1.9.0
|
||||
# via flask
|
||||
bs4==0.0.2
|
||||
# via -r requirements.in
|
||||
certifi==2025.8.3
|
||||
# via requests
|
||||
charset-normalizer==3.4.3
|
||||
# via requests
|
||||
click==8.2.1
|
||||
# via flask
|
||||
dill==0.4.0
|
||||
# via pylint
|
||||
feedgen==1.0.0
|
||||
# via -r requirements.in
|
||||
flask==3.1.2
|
||||
# via -r requirements.in
|
||||
idna==3.10
|
||||
# via requests
|
||||
isort==6.0.1
|
||||
# via pylint
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.6
|
||||
# via flask
|
||||
lxml==6.0.1
|
||||
# via feedgen
|
||||
markupsafe==3.0.2
|
||||
# via
|
||||
# flask
|
||||
# jinja2
|
||||
# werkzeug
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
peewee==3.18.2
|
||||
# via -r requirements.in
|
||||
platformdirs==4.4.0
|
||||
# via pylint
|
||||
pylint==3.3.8
|
||||
# via -r requirements-dev.in
|
||||
python-dateutil==2.9.0.post0
|
||||
# via feedgen
|
||||
requests==2.32.5
|
||||
# via -r requirements.in
|
||||
six==1.17.0
|
||||
# via python-dateutil
|
||||
soupsieve==2.8
|
||||
# via beautifulsoup4
|
||||
tomlkit==0.13.3
|
||||
# via pylint
|
||||
typing-extensions==4.15.0
|
||||
# via beautifulsoup4
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
werkzeug==3.1.3
|
||||
# via flask
|
||||
@@ -1,7 +1,10 @@
|
||||
pkg-resources==0.0.0
|
||||
|
||||
# Core application
|
||||
flask
|
||||
peewee
|
||||
flask-peewee
|
||||
|
||||
# Fetch title etc from links
|
||||
bs4
|
||||
requests
|
||||
|
||||
# Generate (atom) feeds for tags and such
|
||||
feedgen
|
||||
|
||||
@@ -1,24 +1,47 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile --output-file requirements.txt requirements.in
|
||||
#
|
||||
beautifulsoup4==4.6.0 # via bs4
|
||||
bs4==0.0.1
|
||||
certifi==2017.4.17 # via requests
|
||||
chardet==3.0.4 # via requests
|
||||
click==6.7 # via flask
|
||||
flask-peewee==0.6.7
|
||||
flask==0.12.2
|
||||
idna==2.5 # via requests
|
||||
itsdangerous==0.24 # via flask
|
||||
jinja2==2.9.6 # via flask, flask-peewee
|
||||
markupsafe==1.0 # via jinja2
|
||||
peewee==2.10.1
|
||||
pkg-resources==0.0.0
|
||||
requests==2.18.1
|
||||
urllib3==1.21.1 # via requests
|
||||
werkzeug==0.12.2 # via flask, flask-peewee
|
||||
wtf-peewee==0.2.6 # via flask-peewee
|
||||
wtforms==2.1 # via flask-peewee, wtf-peewee
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in
|
||||
beautifulsoup4==4.13.5
|
||||
# via bs4
|
||||
blinker==1.9.0
|
||||
# via flask
|
||||
bs4==0.0.2
|
||||
# via -r requirements.in
|
||||
certifi==2025.8.3
|
||||
# via requests
|
||||
charset-normalizer==3.4.3
|
||||
# via requests
|
||||
click==8.2.1
|
||||
# via flask
|
||||
feedgen==1.0.0
|
||||
# via -r requirements.in
|
||||
flask==3.1.2
|
||||
# via -r requirements.in
|
||||
idna==3.10
|
||||
# via requests
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.6
|
||||
# via flask
|
||||
lxml==6.0.1
|
||||
# via feedgen
|
||||
markupsafe==3.0.2
|
||||
# via
|
||||
# flask
|
||||
# jinja2
|
||||
# werkzeug
|
||||
peewee==3.18.2
|
||||
# via -r requirements.in
|
||||
python-dateutil==2.9.0.post0
|
||||
# via feedgen
|
||||
requests==2.32.5
|
||||
# via -r requirements.in
|
||||
six==1.17.0
|
||||
# via python-dateutil
|
||||
soupsieve==2.8
|
||||
# via beautifulsoup4
|
||||
typing-extensions==4.15.0
|
||||
# via beautifulsoup4
|
||||
urllib3==2.5.0
|
||||
# via requests
|
||||
werkzeug==3.1.4
|
||||
# via flask
|
||||
|
||||
10
setup.py
10
setup.py
@@ -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.1.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'],
|
||||
install_requires=['Flask', 'Peewee', 'Flask-Peewee', 'requests', 'bs4'],
|
||||
|
||||
py_modules=['digimarks'],
|
||||
|
||||
|
||||
@@ -4,57 +4,24 @@
|
||||
|
||||
/** Navigation **/
|
||||
|
||||
nav .button-collapse
|
||||
nav .sidenav-trigger
|
||||
{
|
||||
/* Fix for misalignment of hamburger icon */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav .button-collapse i
|
||||
nav .sidenav-trigger i
|
||||
{
|
||||
/* Make the hamburger icon great again */
|
||||
font-size: 2.7rem;
|
||||
}
|
||||
|
||||
/** Form input fields **/
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/** Cards and tags **/
|
||||
|
||||
/* Card title anchor colour */
|
||||
.white-text .card-title a,
|
||||
.white-text a
|
||||
.card .card-content,
|
||||
.card .card-reveal
|
||||
{
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.chip a,
|
||||
.white-text .chip a
|
||||
{
|
||||
color: #1b5e20; /* green darken-4 */
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card.tiny
|
||||
@@ -76,17 +43,24 @@ nav .button-collapse i
|
||||
/*display: block;*/
|
||||
}
|
||||
|
||||
.card .digimark-card-content
|
||||
.card .digimark-card-header-tags
|
||||
{
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.card-image i
|
||||
.card-image
|
||||
{
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.card-image i,
|
||||
.list-image i
|
||||
{
|
||||
padding: 5px 0 0 15px;
|
||||
}
|
||||
|
||||
.card.horizontal .card-image img.favicon
|
||||
.card.horizontal .card-image img.favicon,
|
||||
.list-image img.favicon
|
||||
{
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
|
||||
BIN
static/faviconfallback.png
Normal file
BIN
static/faviconfallback.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 B |
@@ -1,7 +1,11 @@
|
||||
(function($){
|
||||
$(function(){
|
||||
/* global M */
|
||||
|
||||
$('.button-collapse').sideNav();
|
||||
var options = {};
|
||||
var elem = document.querySelector(".sidenav");
|
||||
var instance = M.Sidenav.init(elem, options);
|
||||
|
||||
}); // end of document ready
|
||||
})(jQuery); // end of jQuery name space
|
||||
elem = document.querySelector(".collapsible");
|
||||
instance = M.Collapsible.init(elem, {
|
||||
// inDuration: 1000,
|
||||
// outDuration: 1000
|
||||
});
|
||||
|
||||
@@ -8,48 +8,98 @@
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"/>
|
||||
|
||||
<!-- Chrome, Firefox OS and Opera -->
|
||||
<meta name="theme-color" content="#2e7d32" />
|
||||
<meta name="theme-color" content="{{ theme.BROWSERCHROME }}" />
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#2e7d32">
|
||||
<meta name="msapplication-navbutton-color" content="{{ theme.BROWSERCHROME }}">
|
||||
<!-- iOS Safari -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
||||
<meta name="referrer" content="never">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
|
||||
<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.100.1/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>
|
||||
<style>
|
||||
a
|
||||
{
|
||||
color: {{ theme.LINK_TEXT }};
|
||||
}
|
||||
.card-content a
|
||||
{
|
||||
color: {{ theme.CARD_LINK }};
|
||||
}
|
||||
.chip a
|
||||
{
|
||||
color: {{ theme.CHIP_TEXT }};
|
||||
}
|
||||
/* label color */
|
||||
.input-field label
|
||||
.input-field .prefix ~ input, .input-field .prefix ~ textarea, .input-field .prefix ~ label, .input-field .prefix ~ .validate ~ label, .input-field .prefix ~ .autocomplete-content, .input-field input[type=text]
|
||||
{
|
||||
color: {{ theme.TEXTHEX }};
|
||||
}
|
||||
/* label focus color */
|
||||
.input-field input[type=text]:focus + label
|
||||
.input-field input[type=text]:focus + label,
|
||||
.input-field .prefix ~ input[type=text]:focus + label
|
||||
{
|
||||
color: {{ theme.TEXTHEX }};
|
||||
color: {{ theme.BUTTON }};
|
||||
}
|
||||
/* label underline focus color */
|
||||
.input-field input[type=text]:focus,
|
||||
.input-field .prefix ~ input[type=text]:focus,
|
||||
.input-field input[type=text].autocomplete:focus
|
||||
{
|
||||
border-bottom: 1px solid {{ theme.BUTTON }};
|
||||
box-shadow: 0 1px 0 0 {{ theme.BUTTON }};
|
||||
}
|
||||
/* icon prefix focus color */
|
||||
.input-field .prefix.active
|
||||
{
|
||||
color: {{ theme.BUTTON }};
|
||||
}
|
||||
.btn, .btn:visited
|
||||
{
|
||||
background-color: {{ theme.BUTTON }};
|
||||
}
|
||||
.btn:hover, .btn:active
|
||||
{
|
||||
background-color: {{ theme.BUTTON_ACTIVE }};
|
||||
}
|
||||
.deletebtn
|
||||
{
|
||||
background-color: red;
|
||||
}
|
||||
.deletebtn:hover
|
||||
{
|
||||
background-color: #ef5350; /* red lighten-1 */
|
||||
}
|
||||
</style>
|
||||
<link href="{{ url_for('static', filename='css/digimarks.css') }}?20170722" type="text/css" rel="stylesheet" media="screen,projection"/>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<link href="{{ url_for('static', filename='css/digimarks.css') }}?20180330" type="text/css" rel="stylesheet" media="screen,projection"/>
|
||||
{% if not sortmethod %}
|
||||
{% set sortmethod = None %}
|
||||
{% endif %}
|
||||
{% if not show_as %}
|
||||
{% set show_as = None %}
|
||||
{% endif %}
|
||||
</head>
|
||||
<body class="{{ theme.BODY }} {{ theme.TEXT }}">
|
||||
<nav class="{{ theme.NAV }}" 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>
|
||||
<div class="nav-wrapper container"><a id="logo-container" href="{% if userkey %}{{ url_for('bookmarks_page', userkey=userkey, sortmethod=sortmethod, show_as=show_as) }}{% 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>
|
||||
<li><a href="{{ url_for('tags_page', userkey=userkey) }}" class="waves-effect waves-light btn"><i class="material-icons left">label</i>Tags</a></li>
|
||||
<li><a href="{{ url_for('addbookmark', userkey=userkey) }}" class="waves-effect waves-light btn"><i class="material-icons left">add</i>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 id="nav-mobile" class="sidenav">
|
||||
<li><a class="waves-effect" href="{{ url_for('bookmarks_page', userkey=userkey) }}"><i class="material-icons left">turned_in</i>Home</a></li>
|
||||
<li><a class="waves-effect" href="{{ url_for('tags_page', userkey=userkey) }}"><i class="material-icons left">label</i>Tags</a></li>
|
||||
<li><a class="waves-effect" href="{{ url_for('addbookmark', userkey=userkey) }}"><i class="material-icons left">add</i>Add bookmark</a></li>
|
||||
</ul>
|
||||
<a href="#" data-activates="nav-mobile" class="button-collapse"><i class="material-icons">menu</i></a>
|
||||
<a href="#" data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
@@ -68,8 +118,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.1/js/materialize.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/init.js') }}"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/init.js') }}?20180309"></script>
|
||||
|
||||
{% block extrajs %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{% if tag and publictag %}
|
||||
<div class="row">
|
||||
<div class="col s12"><a href="{{ url_for('publictag', tagkey=publictag.tagkey) }}">Public link</a></div>
|
||||
<div class="col s12"><a href="{{ url_for('publictag_page', tagkey=publictag.tagkey) }}">Public link</a></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -33,13 +33,19 @@
|
||||
{% 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" />
|
||||
<form action="{{ url_for('bookmarks_page', userkey=userkey) }}" name="filterForm" method="POST" autocomplete="off">
|
||||
<div class="input-field col l9 m9 s8">
|
||||
<input placeholder="search text" type="text" name="filter_text" id="filter_text" class="autocomplete" value="{{ filter_text }}" autocomplete="false" />
|
||||
</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 class="input-field col l3 m3 s4">
|
||||
<p class="right-align"><button class="btn waves-effect waves-light" type="submit" name="submitBtn" title="Find"><i class="material-icons">search</i></button>
|
||||
{% if show_as and show_as == 'list' %}
|
||||
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod=filtermethod, sortmethod=sortmethod, show_as=None) }}" class="waves-effect waves-light btn" title="Show as cards"><i class="material-icons">apps</i></a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod=filtermethod, sortmethod=sortmethod, show_as='list') }}" class="waves-effect waves-light btn" title="Show as list"><i class="material-icons">reorder</i></a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -52,17 +58,17 @@
|
||||
<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 {{ theme.STAR }}">star</i></a>
|
||||
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod='starred') }}"><i class="tiny material-icons {{ theme.STAR }}">star</i></a>
|
||||
</div>
|
||||
<div class="chip">
|
||||
<a href="{{ url_for('bookmarks', userkey=userkey, filtermethod='broken') }}"><i class="tiny material-icons {{ theme.PROBLEM }}">report_problem</i></a>
|
||||
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod='broken') }}"><i class="tiny material-icons {{ theme.PROBLEM }}">report_problem</i></a>
|
||||
</div>
|
||||
<div class="chip">
|
||||
<a href="{{ url_for('bookmarks', userkey=userkey, filtermethod='note') }}"><i class="tiny material-icons {{ theme.COMMENT }}">comment</i></a>
|
||||
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod='note') }}"><i class="tiny material-icons {{ theme.COMMENT }}">comment</i></a>
|
||||
</div>
|
||||
{% for tag in tags %}
|
||||
<div class="chip">
|
||||
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
|
||||
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag) }}">{{ tag }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</li>
|
||||
@@ -71,74 +77,11 @@
|
||||
</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 {{ theme.CARD_BACKGROUND }}">
|
||||
<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 {{ theme.PROBLEM }}" title="HTTP status {{ bookmark.http_status }}">report_problem</i></div>
|
||||
{% endif %}
|
||||
{% if bookmark.starred == True %}
|
||||
<div><i class="small material-icons {{ theme.STAR }}">star</i></div>
|
||||
{% endif %}
|
||||
{% if bookmark.note %}
|
||||
<div><i class="small material-icons {{ theme.CARD_TEXT }}" title="{{ bookmark.note|truncate(100) }}">comment</i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-stacked">
|
||||
<div class="card-content {{ theme.CARD_TEXT }}">
|
||||
<span class="digimark-card-header activator">
|
||||
<span class="digimark-card-header-tags">
|
||||
{% for tag in bookmark.tags_list %}
|
||||
<div class="chip">
|
||||
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</span>
|
||||
<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 {{ theme.CARD_BACKGROUND }}">
|
||||
<span class="card-title {{ theme.CARD_TEXT }}">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
|
||||
<div class="{{ theme.CARD_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>
|
||||
{% if show_as and show_as == 'list' %}
|
||||
{% include 'list.html' %}
|
||||
{% else %}
|
||||
{% include 'cards.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="fixed-action-btn" style="bottom: 20px; right: 20px;">
|
||||
<a class="btn-floating btn-large {{ theme.FAB }}" href="{{ url_for('addbookmark', userkey=userkey) }}">
|
||||
@@ -146,3 +89,25 @@
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
function submitFilter() {
|
||||
document.filterForm.submit();
|
||||
}
|
||||
/* Search filter autocomplete */
|
||||
var options = {
|
||||
onAutocomplete: submitFilter,
|
||||
minLength: 3,
|
||||
limit: 10,
|
||||
data: {
|
||||
},
|
||||
}
|
||||
var elem = document.querySelector('.autocomplete');
|
||||
var instance = M.Autocomplete.init(elem, options);
|
||||
/* TODO: fetch from API
|
||||
instance.updateData({
|
||||
});
|
||||
*/
|
||||
</script>
|
||||
<script src="{{ url_for('bookmarks_js', userkey=userkey) }}" ></script>
|
||||
{% endblock %}
|
||||
|
||||
11
templates/bookmarks.js
Normal file
11
templates/bookmarks.js
Normal file
@@ -0,0 +1,11 @@
|
||||
var elem = document.querySelector('.autocomplete');
|
||||
var instance = M.Autocomplete.getInstance(elem);
|
||||
instance.updateData({
|
||||
{% for bookmark in bookmarks %}
|
||||
{% if bookmark.favicon %}
|
||||
"{{ bookmark.title | replace('"', '\\"') | replace('\n', '') | replace('\r', '') }}": "{{ url_for('static', filename='favicons/' + bookmark.favicon) }}",
|
||||
{% else %}
|
||||
"{{ bookmark.title | replace('"', '\\"') | replace('\n', '') | replace('\r', '') }}": null,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
65
templates/cards.html
Normal file
65
templates/cards.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<div class="row">
|
||||
{% for bookmark in bookmarks %}
|
||||
<div class="col s12 m6 l4">
|
||||
<div class="card horizontal tiny {{ theme.CARD_BACKGROUND }}">
|
||||
<div class="card-image">
|
||||
{% if bookmark.favicon %}
|
||||
<div><img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" class="favicon" /></div>
|
||||
{% else %}
|
||||
<div><img src="{{ url_for('static', filename='faviconfallback.png') }}" class="favicon" /></div>
|
||||
{% endif %}
|
||||
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
|
||||
<div><i class="small material-icons {{ theme.PROBLEM }}" title="HTTP status {{ bookmark.http_status }}">report_problem</i></div>
|
||||
{% endif %}
|
||||
{% if bookmark.starred == True %}
|
||||
<div><i class="small material-icons {{ theme.STAR }}">star</i></div>
|
||||
{% endif %}
|
||||
{% if bookmark.note %}
|
||||
<div><i class="small material-icons {{ theme.CARD_TEXT }}" title="{{ bookmark.note|truncate(100) }}">comment</i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-stacked">
|
||||
<div class="card-content {{ theme.CARD_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 {{ theme.CARD_BACKGROUND }}">
|
||||
<span class="card-title {{ theme.CARD_TEXT }}">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
|
||||
{% if editable %}
|
||||
<div class="{{ theme.CARD_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>
|
||||
{% endif %}
|
||||
{% if showtags %}
|
||||
<div class="digimark-card-header-tags">
|
||||
{% for tag in bookmark.tags_list %}
|
||||
<div class="chip">
|
||||
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag) }}">{{ tag }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</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>
|
||||
@@ -3,13 +3,15 @@
|
||||
{% block pageheader %}{{ action }}{% endblock %}
|
||||
{% block pagecontent %}
|
||||
|
||||
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
|
||||
{% if bookmark.http_status != 200 and bookmark.http_status != 202 and bookmark.http_status != 304 %}
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<div class="card-panel {{ theme.ERRORMESSAGE_BACKGROUND }}">
|
||||
<span class="{{ theme.ERRORMESSAGE_TEXT }}">
|
||||
{% if bookmark.http_status == 404 %}
|
||||
<i class="material-icons">report_problem</i> URL not found (404), broken/outdated link?
|
||||
{% elif bookmark.http_status == 301 %}
|
||||
<i class="material-icons">report_problem</i> HTTP status (301), moved permanently. Use button for new target
|
||||
{% elif bookmark.http_status == 302 %}
|
||||
<i class="material-icons">report_problem</i> HTTP status (302), moved temporarily. Use button for new target
|
||||
{% elif bookmark.http_status == bookmark.HTTP_CONNECTIONERROR %}
|
||||
@@ -36,46 +38,45 @@
|
||||
{% endif %}
|
||||
|
||||
{% if formaction and formaction == 'edit' %}
|
||||
<form class="digimark" action="{{ url_for('editingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST">
|
||||
<form class="digimark" id="digimark" action="{{ url_for('editingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST" onsubmit="return onSubmitForm();" autocomplete="off">
|
||||
{% else %}
|
||||
<form class="digimark" action="{{ url_for('addingbookmark', userkey=userkey) }}" method="POST">
|
||||
<form class="digimark" id="digimark" action="{{ url_for('addingbookmark', userkey=userkey) }}" method="POST" onsubmit="return onSubmitForm();" autocomplete="off">
|
||||
{% 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" />
|
||||
<input placeholder="title (leave empty for autofetch)" type="text" name="title" id="title" value="{{ bookmark.title }}" autocomplete="false" />
|
||||
<label for="title">Title</label>
|
||||
{# <span class="helper-text">Leave title empty for autofetching from the page</span>#}
|
||||
</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" />
|
||||
<input placeholder="url" type="text" name="url" id="url" value="{{ bookmark.url }}" autocomplete="false" />
|
||||
<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>
|
||||
<a class="waves-effect waves-light btn" id="btn_urlupdate" onclick="updateURL()"><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() }}');
|
||||
});
|
||||
});
|
||||
function updateURL() {
|
||||
var text = document.getElementById('url');
|
||||
text.value = '{{ 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" />
|
||||
<input placeholder="note" type="text" name="note" id="note" value="{{ bookmark.note }}" autocomplete="false" />
|
||||
<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" />
|
||||
<input placeholder="tags, divided by comma's" type="text" name="tags" id="tags" value="{{ bookmark.tags }}" autocomplete="false" />
|
||||
<label for="tags">Tags</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +88,7 @@
|
||||
<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 }}">
|
||||
<div class="chip clickable" id="chip_{{ tag }}" onclick="addTag('{{ tag }}');">
|
||||
{{ tag }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -95,29 +96,23 @@
|
||||
</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">
|
||||
<div class="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>
|
||||
<label>
|
||||
<input type="checkbox" name="starred" id="starred" {% if bookmark.starred == True %}checked{% endif %} />
|
||||
<span>Starred</span>
|
||||
</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 class="col s12">
|
||||
<label>
|
||||
<input type="checkbox" name="strip" id="strip" />
|
||||
<span>Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if bookmark.url_hash %}
|
||||
@@ -152,9 +147,9 @@
|
||||
</div>
|
||||
{% if bookmark.url_hash %}
|
||||
</form>
|
||||
<div class="input-field col l2 m3 s4">
|
||||
<div class="input-field col l4 m4 s6">
|
||||
<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>
|
||||
<p class="left-align"><button class="btn waves-effect waves-light deletebtn" type="submit" name="delete">Delete <i class="material-icons right">delete</i></button></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,9 +159,17 @@
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
console.log('woei');
|
||||
$('form.digimark').on('submit',function(){$("#submit").prop("disabled", true); return true;})
|
||||
});
|
||||
function onSubmitForm()
|
||||
{
|
||||
var theForm = document.getElementById('digimark');
|
||||
var submitButton = document.getElementById('submit');
|
||||
theForm.onsubmit = submitButton.setAttribute("disabled", true);
|
||||
return true;
|
||||
}
|
||||
function addTag(tagText)
|
||||
{
|
||||
var text = document.getElementById('tags');
|
||||
text.value = text.value + ', ' + tagText;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
62
templates/list.html
Normal file
62
templates/list.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<div class="row">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>Bookmark</th>
|
||||
<th>Added</th>
|
||||
{% if showtags %}
|
||||
<th>Tags</th>
|
||||
{% endif %}
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bookmark in bookmarks %}
|
||||
<tr>
|
||||
<td class="list-image">
|
||||
{% if bookmark.favicon %}
|
||||
<img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" class="favicon" />
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='faviconfallback.png') }}" class="favicon" />
|
||||
{% endif %}
|
||||
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
|
||||
<i class="small material-icons {{ theme.PROBLEM }}" title="HTTP status {{ bookmark.http_status }}">report_problem</i>
|
||||
{% endif %}
|
||||
{% if bookmark.starred == True %}
|
||||
<i class="small material-icons {{ theme.STAR }}">star</i>
|
||||
{% endif %}
|
||||
{% if bookmark.note %}
|
||||
<i class="small material-icons {{ theme.CARD_TEXT }}" title="{{ bookmark.note|truncate(100) }}">comment</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
<td>{{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
{% if showtags %}
|
||||
<td>
|
||||
{% for tag in bookmark.tags_list %}
|
||||
<div class="chip">
|
||||
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag) }}">{{ tag }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if editable %}
|
||||
<a href="{{ url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" class="waves-effect waves-light btn" title="Edit"><i class="tiny material-icons">mode_edit</i></a>
|
||||
<a href="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" class="waves-effect waves-light btn red" title="DELETE"><i class="tiny material-icons">delete</i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -20,49 +20,10 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<a href="{{ url_for('publictagfeed', tagkey=tagkey) }}"><i class="material-icons tiny">rss_feed</i> feed</a>
|
||||
<a href="{{ url_for('publictag_feed', 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>
|
||||
{% include 'cards.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
16
templates/redirect.html
Normal file
16
templates/redirect.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirecting - digimarks</title>
|
||||
<meta name="referrer" content="never">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta http-equiv=refresh content="3; URL={{ url }}">
|
||||
<style>
|
||||
body { background-color: #000; color: #FFF; }
|
||||
a { color: #fb8c00; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>You're being redirected. If nothing happens, <a href="{{ url }}">click here instead</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,11 +44,11 @@
|
||||
{% for tag in tags %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('tag', userkey=userkey, tag=tag['tag']) }}">{{ tag['tag'] }}</a>
|
||||
<a href="{{ url_for('tag_page', 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>)
|
||||
<a href="{{ url_for('publictag_page', 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 %}
|
||||
|
||||
9
wsgi.py
9
wsgi.py
@@ -1,8 +1,13 @@
|
||||
# Activate virtualenv
|
||||
import settings
|
||||
activate_this = getattr(settings, 'VENV', None)
|
||||
if activate_this:
|
||||
execfile(activate_this, dict(__file__=activate_this))
|
||||
# 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user