openmedialibrary/oml/user/models.py

619 lines
21 KiB
Python
Raw Normal View History

2014-05-04 17:26:43 +00:00
# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
from datetime import datetime
2014-05-04 17:26:43 +00:00
import json
2016-01-14 08:59:11 +00:00
import os
import shutil
2014-05-04 17:26:43 +00:00
2016-01-14 08:59:11 +00:00
import ox
from sqlalchemy.orm import load_only
2014-08-09 15:03:16 +00:00
import sqlalchemy as sa
2014-05-04 17:26:43 +00:00
from changelog import Changelog
2014-08-12 08:16:57 +00:00
from db import MutableDict
import db
2014-09-02 22:32:44 +00:00
import json_pickler
2014-05-04 17:26:43 +00:00
import settings
import state
import utils
2016-01-14 08:59:11 +00:00
import media
from websocket import trigger_event
2014-05-04 17:26:43 +00:00
2014-05-17 14:26:59 +00:00
import logging
2015-11-29 14:56:38 +00:00
logger = logging.getLogger(__name__)
2014-05-17 14:26:59 +00:00
2014-05-04 17:26:43 +00:00
class User(db.Model):
2014-08-09 15:03:16 +00:00
__tablename__ = 'user'
2014-05-04 17:26:43 +00:00
2014-08-09 15:03:16 +00:00
created = sa.Column(sa.DateTime())
modified = sa.Column(sa.DateTime())
2014-08-09 15:03:16 +00:00
id = sa.Column(sa.String(43), primary_key=True)
2014-09-02 22:32:44 +00:00
info = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler)))
2014-05-04 17:26:43 +00:00
2014-08-09 15:03:16 +00:00
nickname = sa.Column(sa.String(256), unique=True)
2014-05-04 17:26:43 +00:00
2014-08-09 15:03:16 +00:00
pending = sa.Column(sa.String(64)) # sent|received
queued = sa.Column(sa.Boolean())
peered = sa.Column(sa.Boolean())
online = sa.Column(sa.Boolean())
2014-05-04 17:26:43 +00:00
def __repr__(self):
return self.id
@classmethod
def get(cls, id):
2016-01-17 13:12:56 +00:00
user = cls.query.filter_by(id=id).first()
if user and not user.info:
user.info = {}
return user
2014-05-04 17:26:43 +00:00
@classmethod
def get_or_create(cls, id):
user = cls.get(id)
if not user:
user = cls(id=id, peered=False, online=False)
user.info = {}
2016-03-14 13:31:56 +00:00
if state.nodes and state.nodes.local and id in state.nodes.local:
user.info['local'] = state.nodes.local[id]
2015-12-01 08:59:52 +00:00
user.info['username'] = user.info['local']['username']
user.update_name()
2014-05-04 17:26:43 +00:00
user.save()
return user
def save(self):
2014-08-09 16:14:14 +00:00
state.db.session.add(self)
state.db.session.commit()
2014-05-04 17:26:43 +00:00
2014-05-25 12:16:04 +00:00
@property
def name(self):
name = self.nickname if self.id != settings.USER_ID else ''
return name
@property
def library(self):
l = List.get_or_create(self.id, '')
if l.index_ != -1:
l.index_ = -1
l.save()
return l
def json(self, keys=None):
2014-05-04 17:26:43 +00:00
j = {}
if self.info:
j.update(self.info)
j['id'] = self.id
if self.pending:
j['pending'] = self.pending
j['peered'] = self.peered
2016-02-25 07:39:59 +00:00
if not keys or 'online' in keys:
j['online'] = self.is_online()
2014-05-25 12:16:04 +00:00
j['name'] = self.name
2016-02-25 07:39:59 +00:00
if not keys or 'username' in keys or 'contact' in keys:
if self.id == settings.USER_ID:
j['username'] = settings.preferences['username']
j['contact'] = settings.preferences['contact']
elif self.id in state.peers:
peer = state.peers[self.id]
for key in ('username', 'contact'):
if key in peer.info:
j[key] = peer.info[key]
if keys:
for k in set(j) - set(keys):
del j[k]
2014-05-04 17:26:43 +00:00
return j
def export_library(self):
old_path = os.path.join(os.path.expanduser(settings.preferences['libraryPath']), 'Books', 'library.json')
if os.path.exists(old_path):
os.unlink(old_path)
path = os.path.join(settings.data_path, 'library.json')
self.library.export_json(path)
def is_online(self):
return state.nodes and state.nodes.is_online(self.id)
2014-05-04 17:26:43 +00:00
def trigger_status(self):
trigger_event('status', {
'id': self.id,
'online': self.is_online()
})
2014-05-04 17:26:43 +00:00
def lists_json(self):
self.library
2016-02-11 16:28:46 +00:00
if self.id != settings.USER_ID:
peer = utils.get_peer(self.id)
lists = []
lists.append({
'id': self.nickname + ':',
'user': self.name,
'items': len(peer.library),
'name': 'Library',
'type': 'library'
})
index = 0
2016-02-11 16:52:52 +00:00
for name in peer.info.get('listorder', peer.info.get('lists', []).keys()):
2016-02-11 16:28:46 +00:00
lists.append({
'id': '%s:%s' % (self.nickname, name),
'user': self.name,
'name': name,
'index': index,
'items': len(peer.info['lists'].get(name, [])),
'type': 'static'
})
index += 1
return lists
return [l.json() for l in self.lists.order_by('index_')]
2014-05-04 17:26:43 +00:00
def update_peering(self, peered, username=None):
2014-05-13 10:36:02 +00:00
was_peering = self.peered
2014-05-04 17:26:43 +00:00
if peered:
2015-12-01 08:59:52 +00:00
logging.debug('update_peering, pending: %s queued: %s', self.pending, self.queued)
self.queued = self.pending != 'sent'
2014-05-04 17:26:43 +00:00
self.pending = ''
if username:
self.info['username'] = username
2014-05-25 12:16:04 +00:00
self.update_name()
2014-05-17 11:45:57 +00:00
# FIXME: need to set peered to False to not trigger changelog event
# before other side receives acceptPeering request
self.peered = False
self.save()
2014-05-13 10:36:02 +00:00
if not was_peering:
Changelog.record(state.user(), 'addpeer', self.id, self.nickname)
2016-03-22 13:04:31 +00:00
if not 'index' in self.info:
self.info['index'] = max([
u.info.get('index', -1) for u in User.query.filter_by(peered=True)
]) + 1
2014-05-17 11:45:57 +00:00
self.peered = True
self.save()
if self.id in state.removepeer:
del state.removepeer[self.id]
2014-05-04 17:26:43 +00:00
else:
2014-05-13 10:36:02 +00:00
self.pending = ''
2014-05-04 17:26:43 +00:00
self.peered = False
2015-12-01 08:59:52 +00:00
self.queued = False
2016-03-22 13:04:31 +00:00
if 'index' in self.info:
del self.info['index']
2014-05-25 12:16:04 +00:00
self.update_name()
2014-05-17 11:45:57 +00:00
self.save()
if self.name in settings.ui['showFolder']:
del settings.ui['showFolder'][self.name]
settings.ui._save()
state.removepeer[self.id] = True
self.cleanup()
2014-05-13 10:36:02 +00:00
if was_peering:
Changelog.record(state.user(), 'removepeer', self.id)
self.save()
def cleanup(self):
2016-02-11 17:55:46 +00:00
from item.models import user_items, Item
List.query.filter_by(user_id=self.id).delete()
2016-02-11 17:55:46 +00:00
c_user_id = user_items.columns['user_id']
q = user_items.delete().where(c_user_id.is_(self.id))
state.db.session.execute(q)
Item.remove_without_user()
2014-05-04 17:26:43 +00:00
self.save()
2016-02-10 14:02:32 +00:00
if self.id in state.peers:
state.peers[self.id].remove()
del state.peers[self.id]
2014-05-04 17:26:43 +00:00
2014-05-25 12:16:04 +00:00
def update_name(self):
if self.id == settings.USER_ID:
name = settings.preferences.get('username', 'anonymous')
else:
name = self.info.get('nickname') or self.info.get('username') or 'anonymous'
nickname = name
2014-05-04 17:26:43 +00:00
n = 2
while self.query.filter_by(nickname=nickname).filter(User.id!=self.id).first():
2014-05-25 12:16:04 +00:00
nickname = '%s [%d]' % (name, n)
2014-05-04 17:26:43 +00:00
n += 1
self.nickname = nickname
def rebuild_changelog(self):
Changelog.query.filter_by(user_id=self.id).delete()
for item in self.library.get_items().order_by('created'):
2016-01-24 06:35:58 +00:00
Changelog.record(self, 'additem', item.id, item.info, _commit=False)
Changelog.record(self, 'edititem', item.id, item.meta, _commit=False)
lists = []
for l in List.query.filter_by(user_id=self.id, type='static').order_by('index_'):
if l.name:
lists.append(l.name)
2016-01-24 06:35:58 +00:00
Changelog.record(self, 'addlist', l.name, _commit=False)
items = [i.id for i in l.get_items().options(load_only('id'))]
if items:
2016-01-24 06:35:58 +00:00
Changelog.record(self, 'addlistitems', l.name, items, _commit=False)
if len(lists) > 1:
2016-01-24 06:35:58 +00:00
Changelog.record(self, 'orderlists', lists, _commit=False)
for peer in User.query.filter_by(peered=True):
2016-01-24 06:35:58 +00:00
Changelog.record(self, 'addpeer', peer.id, self.nickname, _commit=False)
if peer.info.get('contact'):
Changelog.record(self, 'editpeer', peer.id, {
'contact': peer.info.get('contact')
2016-01-24 06:35:58 +00:00
}, _commit=False)
if settings.preferences.get('contact'):
2016-01-24 06:35:58 +00:00
Changelog.record(self, 'editcontact', settings.preferences.get('contact'), _commit=False)
state.db.session.commit()
2014-08-09 16:14:14 +00:00
list_items = sa.Table('listitem', db.metadata,
2014-08-09 15:03:16 +00:00
sa.Column('list_id', sa.Integer(), sa.ForeignKey('list.id')),
sa.Column('item_id', sa.String(32), sa.ForeignKey('item.id'))
2014-05-04 17:26:43 +00:00
)
class List(db.Model):
2014-08-09 15:03:16 +00:00
__tablename__ = 'list'
2014-05-04 17:26:43 +00:00
2014-08-09 15:03:16 +00:00
id = sa.Column(sa.Integer(), primary_key=True)
name = sa.Column(sa.String())
index_ = sa.Column(sa.Integer())
2014-05-04 17:26:43 +00:00
2014-08-09 15:03:16 +00:00
type = sa.Column(sa.String(64))
2014-09-02 22:32:44 +00:00
_query = sa.Column('query', MutableDict.as_mutable(sa.PickleType(pickler=json_pickler)))
2014-05-04 17:26:43 +00:00
2014-08-09 15:03:16 +00:00
user_id = sa.Column(sa.String(43), sa.ForeignKey('user.id'))
user = sa.orm.relationship('User', backref=sa.orm.backref('lists', lazy='dynamic'))
items = sa.orm.relationship('Item', secondary=list_items,
backref=sa.orm.backref('lists', lazy='dynamic'))
2014-05-04 17:26:43 +00:00
@classmethod
def get(cls, user_id, name=None):
2014-05-18 23:24:04 +00:00
if name is None:
2014-05-04 17:26:43 +00:00
user_id, name = cls.get_user_name(user_id)
return cls.query.filter_by(user_id=user_id, name=name).first()
@classmethod
def get_user_name(cls, user_id):
2014-05-18 23:24:04 +00:00
nickname, name = user_id.split(':', 1)
2014-05-04 17:26:43 +00:00
if nickname:
user = User.query.filter_by(nickname=nickname).first()
user_id = user.id
else:
user_id = settings.USER_ID
return user_id, name
@classmethod
2014-05-18 23:24:04 +00:00
def get_or_create(cls, user_id, name=None, query=None):
if name is None:
2014-05-04 17:26:43 +00:00
user_id, name = cls.get_user_name(user_id)
l = cls.get(user_id, name)
if not l:
2014-05-18 23:24:04 +00:00
l = cls.create(user_id, name, query)
2014-05-04 17:26:43 +00:00
return l
@classmethod
def create(cls, user_id, name, query=None):
2014-05-18 23:24:04 +00:00
prefix = name
n = 2
while cls.get(user_id, name):
name = '%s [%s]' % (prefix, n)
n += 1
l = cls(user_id=user_id, name=name)
2014-05-04 17:26:43 +00:00
l._query = query
l.type = 'smart' if l._query else 'static'
l.index_ = cls.query.filter_by(user_id=user_id).count()
2014-08-09 16:14:14 +00:00
state.db.session.add(l)
state.db.session.commit()
2014-05-04 17:26:43 +00:00
if user_id == settings.USER_ID:
if not l._query and name != '':
2014-05-25 18:06:12 +00:00
Changelog.record(state.user(), 'addlist', l.name)
2014-05-04 17:26:43 +00:00
return l
2014-05-25 18:06:12 +00:00
@classmethod
def rename_user(cls, old, new):
for l in cls.query.filter(cls._query!=None):
def update_conditions(conditions):
changed = False
for c in conditions:
if 'conditions' in c:
changed = update_conditions(c['conditions'] )
2014-05-25 18:06:12 +00:00
else:
if c.get('key') == 'list' and c.get('value', '').startswith('%s:' % old):
c['value'] = '%s:%s' % new, c['value'].split(':', 1)[1]
changed = True
return changed
if update_conditions(l._query.get('conditions', [])):
l.save()
2016-02-10 14:02:32 +00:00
def add_items(self, items, commit=True):
2014-05-04 17:26:43 +00:00
from item.models import Item
available_items = []
2014-05-04 17:26:43 +00:00
for item_id in items:
i = Item.get(item_id)
if i:
if self.user_id == settings.USER_ID and i.info.get('mediastate') != 'available':
2016-02-24 06:52:36 +00:00
i.queue_download()
if i not in self.items:
self.items.append(i)
2016-02-15 11:29:58 +00:00
i.update(commit=False)
if i.info['mediastate'] == 'available':
available_items.append(item_id)
2014-08-09 16:14:14 +00:00
state.db.session.add(self)
2016-02-10 14:02:32 +00:00
if commit:
state.db.session.commit()
if self.user_id == settings.USER_ID and self.name != '' and available_items:
Changelog.record(self.user, 'addlistitems', self.name, available_items)
def get_items(self):
from item.models import Item
if self.type == 'smart':
return Item.find({'query': self._query})
else:
return self.user.items.join(Item.lists, aliased=True).filter(List.id == self.id)
2014-05-04 17:26:43 +00:00
2016-02-10 14:02:32 +00:00
def remove_items(self, items, commit=True):
2014-05-04 17:26:43 +00:00
from item.models import Item
for item_id in items:
i = Item.get(item_id)
if i:
if i in self.items:
self.items.remove(i)
2016-02-10 14:02:32 +00:00
i.update(commit=commit)
2014-08-09 16:14:14 +00:00
state.db.session.add(self)
2016-02-10 14:02:32 +00:00
if commit:
state.db.session.commit()
if self.user_id == settings.USER_ID and self.name != '':
2014-05-12 12:57:47 +00:00
Changelog.record(self.user, 'removelistitems', self.name, items)
2014-05-04 17:26:43 +00:00
2016-02-10 14:02:32 +00:00
def remove(self, commit=True):
2014-05-04 17:26:43 +00:00
if not self._query:
2016-02-10 14:02:32 +00:00
q = list_items.delete().where(list_items.columns['list_id'].is_(self.id))
state.db.session.execute(q)
2014-05-04 17:26:43 +00:00
if not self._query:
if self.user_id == settings.USER_ID and self.name != '':
2014-05-12 12:57:47 +00:00
Changelog.record(self.user, 'removelist', self.name)
2014-08-09 16:14:14 +00:00
state.db.session.delete(self)
2016-02-10 14:02:32 +00:00
if commit:
state.db.session.commit()
2014-05-04 17:26:43 +00:00
@property
def public_id(self):
2014-05-12 12:57:47 +00:00
id = ''
if self.user_id != settings.USER_ID:
id += self.user.nickname
2014-09-02 22:32:44 +00:00
id = '%s:%s' % (id, self.name)
2014-05-12 12:57:47 +00:00
return id
@property
def find_id(self):
2014-05-04 17:26:43 +00:00
id = ''
if self.user_id != settings.USER_ID:
id += self.user_id
2014-09-02 22:32:44 +00:00
id = '%s:%s' % (id, self.id)
2014-05-04 17:26:43 +00:00
return id
2016-01-04 10:23:54 +00:00
2014-05-12 12:57:47 +00:00
def __repr__(self):
2015-03-07 16:19:24 +00:00
return self.public_id
2014-05-04 17:26:43 +00:00
def items_count(self):
2016-02-11 15:55:16 +00:00
if self.user_id != settings.USER_ID:
peer = utils.get_peer(self.user_id)
if self.name:
return len(peer.info['lists'].get(self.name, []))
else:
return len(peer.library)
return self.get_items().count()
2014-05-04 17:26:43 +00:00
def json(self):
r = {
'id': self.public_id,
2014-05-25 12:16:04 +00:00
'user': self.user.name,
2014-05-04 17:26:43 +00:00
'name': self.name,
'index': self.index_,
'items': self.items_count(),
2014-05-04 17:26:43 +00:00
'type': self.type
}
if self.name == '':
r['name'] = 'Library'
r['type'] = 'library'
del r['index']
2014-05-04 17:26:43 +00:00
if self.type == 'smart':
r['query'] = self._query
return r
def save(self):
2014-08-09 16:14:14 +00:00
state.db.session.add(self)
state.db.session.commit()
2016-01-14 08:59:11 +00:00
def create_symlinks(self):
pass
2016-01-15 07:59:35 +00:00
def export_json(self, path=None):
2016-01-15 08:28:42 +00:00
from utils import _to_json
2016-01-15 07:59:35 +00:00
if not path:
if self.name:
name = os.path.join('Lists', self.name)
else:
name = 'Books'
path = os.path.join(os.path.expanduser(settings.preferences['libraryPath']), name, 'library.json')
2016-01-15 08:28:42 +00:00
ox.makedirs(os.path.dirname(path))
2016-01-15 07:59:35 +00:00
items = []
for i in self.get_items():
j = i.json()
for f in i.files:
j['path'] = f.path
break
items.append(j)
with open(path, 'w') as f:
2016-01-17 09:00:11 +00:00
json.dump(items, f, indent=4, default=_to_json, ensure_ascii=False, sort_keys=True)
2016-01-15 07:59:35 +00:00
class Metadata(db.Model):
__tablename__ = 'user_metadata'
created = sa.Column(sa.DateTime())
modified = sa.Column(sa.DateTime())
id = sa.Column(sa.Integer(), primary_key=True)
item_id = sa.Column(sa.String(32))
user_id = sa.Column(sa.String(43), sa.ForeignKey('user.id'))
data_hash = sa.Column(sa.String(40), index=True)
data = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler)))
def __repr__(self):
return '{item}/{user}'.format(item=self.item_id, user=self.user_id)
@property
def timestamp(self):
return utils.datetime2ts(self.modified)
@classmethod
def get(cls, user_id, item_id):
2016-01-19 10:05:16 +00:00
return cls.query.filter_by(item_id=item_id, user_id=user_id).first()
@classmethod
def get_or_create(cls, user_id, item_id, data=None, commit=True):
m = cls.get(user_id=user_id, item_id=item_id)
if not m:
m = cls(user_id=user_id, item_id=item_id)
m.created = datetime.utcnow()
if data:
m.data = data
else:
m.data = {}
2016-01-19 10:05:16 +00:00
m.save(commit=commit)
elif data:
2016-01-19 10:05:16 +00:00
m.edit(data, commit=commit)
return m
2016-01-19 10:05:16 +00:00
def get_hash(self):
2016-02-10 14:02:32 +00:00
return utils.get_meta_hash(self.data)
2016-01-19 10:05:16 +00:00
def save(self, commit=True, modified=None):
if modified is None:
self.modified = datetime.utcnow()
else:
self.modified = modified
state.db.session.add(self)
if commit:
state.db.session.commit()
2016-01-21 16:10:10 +00:00
def edit(self, data, commit=True, modified=None):
changes = {}
if 'isbn' in data and isinstance(data['isbn'], list):
isbns = [utils.to_isbn13(isbn) for isbn in data['isbn']]
isbns = [isbn for isbn in isbns if isbn]
2016-01-12 05:38:40 +00:00
if isbns:
data['isbn'] = isbns[0]
else:
del data['isbn']
for key in data:
if key == 'id':
continue
if data[key] != self.data.get(key):
self.data[key] = data[key]
changes[key] = data[key]
if changes:
2016-01-19 10:05:16 +00:00
self.data_hash = self.get_hash()
self.save(commit=commit, modified=modified)
return changes
def delete(self):
state.db.session.delete(self)
state.db.session.commit()
2016-01-14 12:22:39 +00:00
def export_list(data):
with db.session():
self = List.get(data['list'])
if not self:
return
mode = data.get('mode')
prefix = data.get('path')
if mode not in ('add', 'replace'):
logger.debug('invalid mode %s', mode)
return
if not prefix or prefix == '/':
logger.debug('invalid export path %s', prefix)
trigger_event('activity', {
'activity': 'export',
'path': prefix,
'progress': [0, 0],
'status': {'code': 404, 'text': 'invalid export path'}
})
return
root = prefix
while not os.path.exists(root) and root != '/':
root = os.path.dirname(root)
if not os.access(root, os.W_OK):
logger.debug('can not write to %s', root)
trigger_event('activity', {
'activity': 'export',
'path': prefix,
'progress': [0, 0],
'path': prefix,
'status': {'code': 404, 'text': 'permission denied'}
})
return
if os.path.exists(prefix):
existing_files = set(
os.path.join(root, f) for root, _, files in os.walk(prefix) for f in files
)
else:
existing_files = set()
new_files = set()
count = self.get_items().count()
n = 1
for i in self.get_items():
if i.files.all():
f = i.files.all()[0]
source = f.fullpath()
target = os.path.join(prefix, f.path)
if mode == 'add':
p = 1
parts = target.rsplit('.', 1)
while os.path.exists(target) and media.get_id(target) != f.sha1:
target = '.'.join([parts[0], f.sha1[:p], parts[1]])
p += 1
ox.makedirs(os.path.dirname(target))
if os.path.exists(target):
if mode == 'replace' and media.get_id(target) != f.sha1:
os.unlink(target)
shutil.copy2(source, target)
else:
shutil.copy2(source, target)
new_files.add(target)
trigger_event('activity', {
'activity': 'export',
'path': prefix,
'progress': [n, count]
})
n += 1
if mode == 'replace':
for f in list(existing_files - new_files):
os.unlink(f)
utils.remove_empty_folders(prefix)
2016-01-15 07:59:35 +00:00
self.export_json(os.path.join(prefix, 'library.json'))
2016-01-14 12:22:39 +00:00
trigger_event('activity', {
'activity': 'export',
'progress': [count, count],
'path': prefix,
'status': {'code': 200, 'text': ''},
})
def update_user_peering(user_id, peered, username=None):
with db.session():
u = User.get(user_id)
if u:
u.update_peering(peered, username)
2016-03-14 13:31:56 +00:00
def remove_local_info(id):
with db.session():
u = User.get(id)
if u and 'local' in u.info:
del u.info['local']
u.save()
u.trigger_status()
def add_local_info(data):
with db.session():
u = User.get(data['id'])
if u:
if u.info['username'] != data['username']:
u.info['username'] = data['username']
u.update_name()
u.info['local'] = data
u.save()
state.nodes.queue('add', u.id)