|
import re
|
|
from urllib.request import urlopen
|
|
|
|
import sqlite3
|
|
|
|
from tag import Tag
|
|
|
|
|
|
DB_PATH = 'bookmarks.db'
|
|
|
|
SQL_SELECT_BOOKMARK = "SELECT * FROM bookmarks WHERE id=?"
|
|
SQL_INSERT_BOOKMARK = "INSERT INTO bookmarks VALUES (?,?,?,?,?,?,?,?)"
|
|
SQL_UPDATE_BOOKMARK = "UPDATE bookmarks SET {:s} WHERE id=?"
|
|
SQL_SELECT_CAT_TAGS = "SELECT tag_id FROM category_tags where bookmark_id=?"
|
|
SQL_SELECT_DESC_TAGS = "SELECT tag_id FROM description_tags where bookmark_id=?"
|
|
|
|
HTTP_OKAY = 200
|
|
|
|
class BookmarkNotFoundError(Exception):
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def __str__(self, value):
|
|
return repr(self.value)
|
|
|
|
|
|
class DBConnectionError(Exception):
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def __str__(self, value):
|
|
return repr(self.value)
|
|
|
|
|
|
class Bookmark(object):
|
|
|
|
def __init__(self, db_conn, id=None, url=None):
|
|
|
|
# Make sure the DB connection is valid before doing anything else
|
|
if not db_conn:
|
|
raise DBConnectionError('Invalid database connection object!')
|
|
|
|
# Setup internal parameters
|
|
self._db_conn = db_conn
|
|
self._content = None
|
|
|
|
# Setup defaut object data attributes
|
|
self.id = id
|
|
self.url = url
|
|
self.title = ''
|
|
self.description = ''
|
|
self.times_visited = 0
|
|
self.last_visited = 0
|
|
self.last_reachable = 0
|
|
self.deleted = False
|
|
self.category_tags = set()
|
|
self.description_tags = set()
|
|
|
|
# Retrieve an existing bookmark if an ID is passed in
|
|
if self.id:
|
|
db = self._db_conn.cursor()
|
|
db.execute(SQL_SELECT_BOOKMARK, (self.id,))
|
|
row = db.fetchone()
|
|
|
|
# Check to see if a record was found
|
|
if not row:
|
|
raise BookmarkNotFoundError(self.id)
|
|
|
|
# Assign data attributes
|
|
self.url = row[1]
|
|
self.title = row[2]
|
|
self.description = row[3]
|
|
self.times_visited = int(row[4])
|
|
self.last_visited = row[5]
|
|
self.last_reachable = row[6]
|
|
if int(row[7]) == 0:
|
|
self.deleted = False
|
|
else:
|
|
self.deleted = True
|
|
|
|
# Grab category tags
|
|
for row in db.execute(SQL_SELECT_CAT_TAGS, (self.id,)):
|
|
self.category_tags.add(Tag(int(row[0])))
|
|
|
|
# Grab description tags
|
|
for row in db.execute(SQL_SELECT_DESC_TAGS, (self.id,)):
|
|
self.description_tags.add(Tag(int(row[0])))
|
|
|
|
# This dict MUST appear *after* the data attributes, it is used to
|
|
# record which data attributes are dirty
|
|
self._dirty = {'url': False,
|
|
'title': False,
|
|
'description': False,
|
|
'times_visited': False,
|
|
'last_visited': False,
|
|
'last_reachable': False,
|
|
'deleted': False}
|
|
|
|
def __del__(self):
|
|
""" Automatically save any changes to a record before the object is
|
|
destroyed.
|
|
"""
|
|
self.save()
|
|
|
|
def save(self):
|
|
""" Save any changes to a bookmark record if it is dirty. """
|
|
# Check if the record is dirty
|
|
if self.record_dirty:
|
|
if self.id:
|
|
# Update existing bookmark record
|
|
self._db_update()
|
|
else:
|
|
# Insert new bookmark record
|
|
self._db_insert()
|
|
|
|
def __setattr__(self, name, value):
|
|
""" Customize setting data attributes so we can tell which ones
|
|
are dirty.
|
|
"""
|
|
|
|
# Check if the attr is a data attribute then check if the value has
|
|
# changed. If so, mark the attribute as dirty
|
|
if hasattr(self, '_dirty'):
|
|
if name in self._dirty and getattr(self, name) != value:
|
|
self._dirty[name] = True
|
|
|
|
# Set the attribute value using the super class's __setattr__ to avoid
|
|
# infinite recursion (recursion joke here)
|
|
super(Bookmark, self).__setattr__(name, value)
|
|
|
|
def _db_insert(self):
|
|
""" Insert a new bookmark record into the database. """
|
|
db = self._db_conn.cursor()
|
|
values = (0, self.url, self.title, self.description,
|
|
self.times_visited, self.last_visited, self.last_reachable,
|
|
self.deleted and '1' or '0')
|
|
db.execute(SQL_INSERT_BOOKMARK, values)
|
|
self._db_conn.commit()
|
|
|
|
# Reset dirty flags
|
|
for key in self._dirty.keys():
|
|
self._dirty[key] = False
|
|
|
|
def _db_update(self):
|
|
""" Update an exisitng bookmark record in the database. """
|
|
values = []
|
|
sets = []
|
|
|
|
# Find dirty data attributes for updating
|
|
for key in self._dirty.keys():
|
|
if self._dirty[key] == True:
|
|
sets.append('{:s}=?'.format(key))
|
|
values.append(getattr(self, key))
|
|
|
|
if not values or not sets:
|
|
return
|
|
|
|
# Add the record id as the last value
|
|
values.append(self.id)
|
|
|
|
# Build full SQL statement then execute it
|
|
sql = SQL_UPDATE_BOOKMARK.format(','.join(sets))
|
|
db = self._db_conn.cursor()
|
|
db.execute(sql, values)
|
|
self._db_conn.commit()
|
|
|
|
# Reset dirty flags
|
|
for key in self._dirty.keys():
|
|
self._dirty[key] = False
|
|
|
|
@property
|
|
def record_dirty(self):
|
|
""" Check if any data attributes are dirty, which indicates the record
|
|
is dirty.
|
|
"""
|
|
return any([val for key, val in self._dirty.items()])
|
|
|
|
def _get_content(self):
|
|
""" Read the page and get its raw page HTML content. """
|
|
if self.url:
|
|
response = urlopen(self.url)
|
|
if response.getcode() == HTTP_OKAY:
|
|
self._content = response.read().decode('utf-8')
|
|
|
|
def suggest_title(self):
|
|
""" Return the contents of the page's <title> tag as a title suggestion
|
|
"""
|
|
if self.url and not self._content:
|
|
self._get_content()
|
|
|
|
try:
|
|
title = re.search('<title>(.+)</title>', self._content).group(1)
|
|
except IndexError:
|
|
title = None
|
|
return title
|
|
|
|
def is_reachable(self):
|
|
""" Check if the page is reachable. """
|
|
if self.url:
|
|
response = urlopen(self.url)
|
|
if response.getcode() == HTTP_OKAY:
|
|
return True
|
|
return False
|
|
|
|
|