Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
import hashlib
|
||||
import os.path
|
||||
import platform
|
||||
import re
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
def getDataDir():
|
||||
|
||||
# Windows
|
||||
@@ -102,3 +105,15 @@ def natsortKey(s):
|
||||
|
||||
def natcmp(a, b):
|
||||
return cmp(natsortKey(a), natsortKey(b))
|
||||
|
||||
def getTitle(library_dict):
|
||||
try:
|
||||
try:
|
||||
return library_dict['titles'][0]['title']
|
||||
except:
|
||||
log.error('Could not get title for %s' % library_dict['identifier'])
|
||||
return None
|
||||
except:
|
||||
log.error('Could not get title for library item: %s' % library_dict)
|
||||
return None
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class CoreNotifier(Notification):
|
||||
q.update({Notif.read: True})
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -91,7 +91,7 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
@@ -116,7 +116,7 @@ class CoreNotifier(Notification):
|
||||
ndict['time'] = time.time()
|
||||
self.messages.append(ndict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
def frontend(self, type = 'notification', data = {}):
|
||||
@@ -146,7 +146,7 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
messages.append(ndict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
self.messages = []
|
||||
return jsonified({
|
||||
|
||||
@@ -22,6 +22,6 @@ class History(Notification):
|
||||
)
|
||||
db.add(history)
|
||||
db.commit()
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -87,7 +87,7 @@ class FileManager(Plugin):
|
||||
db.commit()
|
||||
|
||||
type_dict = ft.to_dict()
|
||||
db.close()
|
||||
#db.close()
|
||||
return type_dict
|
||||
|
||||
def getTypes(self):
|
||||
@@ -100,5 +100,5 @@ class FileManager(Plugin):
|
||||
for type_object in results:
|
||||
types.append(type_object.to_dict())
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return types
|
||||
|
||||
@@ -53,7 +53,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
@@ -130,7 +130,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
fireEvent('library.update_finish', data = library_dict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
@@ -144,7 +144,7 @@ class LibraryPlugin(Plugin):
|
||||
db.commit()
|
||||
|
||||
dates = library.info.get('release_date', {})
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return dates
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ class MoviePlugin(Plugin):
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return results
|
||||
|
||||
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
|
||||
@@ -177,7 +177,7 @@ class MoviePlugin(Plugin):
|
||||
})
|
||||
movies.append(temp)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return movies
|
||||
|
||||
def availableChars(self, status = ['active']):
|
||||
@@ -203,7 +203,7 @@ class MoviePlugin(Plugin):
|
||||
if char not in chars:
|
||||
chars += char
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return chars
|
||||
|
||||
def listView(self):
|
||||
@@ -250,7 +250,7 @@ class MoviePlugin(Plugin):
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
|
||||
fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
@@ -324,7 +324,7 @@ class MoviePlugin(Plugin):
|
||||
if (force_readd or do_search) and search_after:
|
||||
fireEventAsync('searcher.single', movie_dict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return movie_dict
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ class MoviePlugin(Plugin):
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('searcher.single', movie_dict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
@@ -426,7 +426,7 @@ class MoviePlugin(Plugin):
|
||||
else:
|
||||
fireEvent('movie.restatus', movie.id, single = True)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
@@ -437,7 +437,7 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
@@ -455,6 +455,6 @@ class MoviePlugin(Plugin):
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -47,7 +47,7 @@ class ProfilePlugin(Plugin):
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(self.to_dict))
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
@@ -84,7 +84,7 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
profile_dict = p.to_dict(self.to_dict)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'profile': profile_dict
|
||||
@@ -95,7 +95,7 @@ class ProfilePlugin(Plugin):
|
||||
db = get_session()
|
||||
default = db.query(Profile).first()
|
||||
default_dict = default.to_dict(self.to_dict)
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return default_dict
|
||||
|
||||
@@ -113,7 +113,7 @@ class ProfilePlugin(Plugin):
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -138,7 +138,7 @@ class ProfilePlugin(Plugin):
|
||||
message = 'Failed deleting Profile: %s' % e
|
||||
log.error(message)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': success,
|
||||
@@ -187,5 +187,5 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
order += 1
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
@@ -68,7 +68,7 @@ class QualityPlugin(Plugin):
|
||||
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
||||
temp.append(q)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def single(self, identifier = ''):
|
||||
@@ -80,7 +80,7 @@ class QualityPlugin(Plugin):
|
||||
if quality:
|
||||
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return quality_dict
|
||||
|
||||
def getQuality(self, identifier):
|
||||
@@ -100,7 +100,7 @@ class QualityPlugin(Plugin):
|
||||
setattr(quality, params.get('value_type'), params.get('value'))
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -152,7 +152,7 @@ class QualityPlugin(Plugin):
|
||||
order += 1
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
def guess(self, files, extra = {}):
|
||||
|
||||
@@ -83,7 +83,7 @@ class Release(Plugin):
|
||||
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -109,7 +109,7 @@ class Release(Plugin):
|
||||
rel.delete()
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -126,7 +126,7 @@ class Release(Plugin):
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -153,14 +153,14 @@ class Release(Plugin):
|
||||
'files': {}
|
||||
}), manual = True)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s' % id)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': False
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile
|
||||
@@ -82,8 +82,10 @@ class Renamer(Plugin):
|
||||
remove_files = []
|
||||
remove_releases = []
|
||||
|
||||
movie_title = getTitle(group['library'])
|
||||
|
||||
# Add _UNKNOWN_ if no library item is connected
|
||||
if not group['library']:
|
||||
if not group['library'] or not movie_title:
|
||||
if group['dirname']:
|
||||
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
|
||||
else: # Add it to filename
|
||||
@@ -100,12 +102,13 @@ class Renamer(Plugin):
|
||||
continue
|
||||
|
||||
library = group['library']
|
||||
movie_title = getTitle(library)
|
||||
|
||||
# Find subtitle for renaming
|
||||
fireEvent('renamer.before', group)
|
||||
|
||||
# Remove weird chars from moviename
|
||||
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', group['library']['titles'][0]['title'])
|
||||
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
||||
|
||||
# Put 'The' at the end
|
||||
name_the = movie_name
|
||||
@@ -369,14 +372,14 @@ class Renamer(Plugin):
|
||||
fireEventAsync('renamer.after', group)
|
||||
|
||||
# Notify on download
|
||||
download_message = 'Downloaded %s (%s)' % (group['library']['titles'][0]['title'], replacements['quality'])
|
||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||
fireEventAsync('movie.downloaded', message = download_message, data = group)
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
self.renaming_started = False
|
||||
|
||||
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
|
||||
|
||||
@@ -8,7 +8,7 @@ from couchpotato.core.settings.model import File
|
||||
from couchpotato.environment import Env
|
||||
from enzyme.exceptions import NoParserError, ParseError
|
||||
from guessit import guess_movie_info
|
||||
from subliminal.videos import scan, Video
|
||||
from subliminal.videos import Video
|
||||
import enzyme
|
||||
import os
|
||||
import re
|
||||
@@ -455,7 +455,7 @@ class Scanner(Plugin):
|
||||
break
|
||||
except:
|
||||
pass
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
# Search based on OpenSubtitleHash
|
||||
if not imdb_id and not group['is_dvd']:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
|
||||
@@ -35,6 +36,6 @@ class Score(Plugin):
|
||||
score += providerScore(nzb['provider'])
|
||||
|
||||
# Duplicates in name
|
||||
score += duplicateScore(nzb['name'], movie['library']['titles'][0]['title'])
|
||||
score += duplicateScore(nzb['name'], getTitle(movie['library']))
|
||||
|
||||
return score
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.variable import md5, getImdb
|
||||
from couchpotato.core.helpers.variable import md5, getImdb, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
@@ -61,7 +61,7 @@ class Searcher(Plugin):
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
self.in_progress = False
|
||||
|
||||
def single(self, movie):
|
||||
@@ -78,7 +78,10 @@ class Searcher(Plugin):
|
||||
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
default_title = movie['library']['titles'][0]['title']
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
return
|
||||
|
||||
for quality_type in movie['profile']['types']:
|
||||
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
|
||||
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
|
||||
@@ -101,6 +104,10 @@ class Searcher(Plugin):
|
||||
if len(sorted_results) == 0:
|
||||
log.debug('Nothing found for %s in %s' % (default_title, quality_type['quality']['label']))
|
||||
|
||||
# Check if movie isn't deleted while searching
|
||||
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
||||
return
|
||||
|
||||
# Add them to this movie releases list
|
||||
for nzb in sorted_results:
|
||||
|
||||
@@ -148,7 +155,7 @@ class Searcher(Plugin):
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return False
|
||||
|
||||
def download(self, data, movie, manual = False):
|
||||
@@ -164,7 +171,7 @@ class Searcher(Plugin):
|
||||
rls.status_id = snatched_status.get('id')
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], rls.quality.label)
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
@@ -191,7 +198,7 @@ class Searcher(Plugin):
|
||||
except Exception, e:
|
||||
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
log.info('Tried to download, but none of the downloaders are enabled')
|
||||
@@ -270,7 +277,7 @@ class Searcher(Plugin):
|
||||
if self.checkNFO(nzb['name'], movie['library']['identifier']):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year']))
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], getTitle(movie['library']), movie['library']['year']))
|
||||
return False
|
||||
|
||||
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):
|
||||
|
||||
@@ -49,7 +49,7 @@ class StatusPlugin(Plugin):
|
||||
db = get_session()
|
||||
status = db.query(Status).filter_by(id = id).first()
|
||||
status_dict = status.to_dict()
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
return status_dict
|
||||
|
||||
@@ -64,7 +64,7 @@ class StatusPlugin(Plugin):
|
||||
s = status.to_dict()
|
||||
temp.append(s)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def add(self, identifier):
|
||||
@@ -82,7 +82,7 @@ class StatusPlugin(Plugin):
|
||||
|
||||
status_dict = s.to_dict()
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return status_dict
|
||||
|
||||
def fill(self):
|
||||
@@ -102,5 +102,5 @@ class StatusPlugin(Plugin):
|
||||
s.label = toUnicode(label)
|
||||
db.commit()
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class Subtitle(Plugin):
|
||||
# get subtitles for those files
|
||||
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
def searchSingle(self, group):
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
// ==/UserScript==
|
||||
|
||||
if (window.top != window.self) // Only run on top window
|
||||
return;
|
||||
|
||||
var version = {{version}},
|
||||
host = '{{host}}',
|
||||
api = '{{api}}';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.metadata.base import MetaDataBase
|
||||
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||
@@ -32,7 +33,7 @@ class XBMC(MetaDataBase):
|
||||
# Title
|
||||
try:
|
||||
el = SubElement(nfoxml, 'title')
|
||||
el.text = toUnicode(data['library']['titles'][0]['title'])
|
||||
el.text = toUnicode(getTitle(data['library']))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class MovieResultModifier(Plugin):
|
||||
except:
|
||||
log.error('Tried getting more info on searched movies: %s' % traceback.format_exc())
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def checkLibrary(self, result):
|
||||
|
||||
@@ -59,7 +59,7 @@ class CouchPotatoApi(MovieProvider):
|
||||
db = get_session()
|
||||
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
|
||||
movies = [x.library.identifier for x in active_movies]
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
suggestions = self.suggest(movies, ignore)
|
||||
|
||||
|
||||
@@ -64,8 +64,12 @@ class IMDBAPI(MovieProvider):
|
||||
movie_data = {}
|
||||
try:
|
||||
|
||||
if isinstance(movie, (str, unicode)):
|
||||
movie = json.loads(movie)
|
||||
try:
|
||||
if isinstance(movie, (str, unicode)):
|
||||
movie = json.loads(movie)
|
||||
except ValueError:
|
||||
log.info('No proper json to decode')
|
||||
return movie_data
|
||||
|
||||
if movie.get('Response') == 'Parse Error':
|
||||
return movie_data
|
||||
|
||||
@@ -69,13 +69,7 @@ class TheMovieDb(MovieProvider):
|
||||
try:
|
||||
nr = 0
|
||||
|
||||
# Sort on returned score first when year is in q
|
||||
if re.search('\s\d{4}', q):
|
||||
movies = sorted(raw, key = lambda k: k['score'], reverse = True)
|
||||
else:
|
||||
movies = raw
|
||||
|
||||
for movie in movies:
|
||||
for movie in raw:
|
||||
results.append(self.parseMovie(movie))
|
||||
|
||||
nr += 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
@@ -26,7 +27,7 @@ class Moovee(NZBProvider):
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']) or quality.get('hd', False):
|
||||
return results
|
||||
|
||||
q = '%s %s' % (movie['library']['titles'][0]['title'], quality.get('identifier'))
|
||||
q = '%s %s' % (getTitle(movie['library']), quality.get('identifier'))
|
||||
url = self.urls['search'] % tryUrlencode(q)
|
||||
|
||||
cache_key = 'moovee.%s' % q
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
@@ -25,7 +25,7 @@ class Mysterbin(NZBProvider):
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||
return results
|
||||
|
||||
q = '"%s" %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
|
||||
q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
for ignored in Env.setting('ignored_words', 'searcher').split(','):
|
||||
q = '%s -%s' % (q, ignored.strip())
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from BeautifulSoup import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
@@ -27,7 +27,7 @@ class NZBClub(NZBProvider, RSS):
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||
return results
|
||||
|
||||
q = '"%s" %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
|
||||
q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
for ignored in Env.setting('ignored_words', 'searcher').split(','):
|
||||
q = '%s -%s' % (q, ignored.strip())
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from BeautifulSoup import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
@@ -29,7 +29,7 @@ class NzbIndex(NZBProvider, RSS):
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['api']):
|
||||
return results
|
||||
|
||||
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
|
||||
q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
arguments = tryUrlencode({
|
||||
'q': q,
|
||||
'age': Env.setting('retention', 'nzb'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
import re
|
||||
@@ -25,7 +25,7 @@ class X264(NZBProvider):
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search'].split('requests')[0]) or not quality.get('hd', False):
|
||||
return results
|
||||
|
||||
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier'))
|
||||
q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
url = self.urls['search'] % tryUrlencode(q)
|
||||
|
||||
cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import StringIO
|
||||
@@ -38,7 +38,7 @@ class KickAssTorrents(TorrentProvider):
|
||||
return results
|
||||
|
||||
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['titles'][0]['title'], movie['library']['identifier'].replace('tt', '')))
|
||||
data = self.getCache(cache_key, self.urls['search'] % (getTitle(movie['library']), movie['library']['identifier'].replace('tt', '')))
|
||||
if data:
|
||||
|
||||
cat_ids = self.getCatId(quality['identifier'])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from BeautifulSoup import SoupStrainer, BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.helpers.variable import mergeDicts, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.trailer.base import TrailerProvider
|
||||
from string import letters, digits
|
||||
@@ -19,7 +19,7 @@ class HDTrailers(TrailerProvider):
|
||||
|
||||
def search(self, group):
|
||||
|
||||
movie_name = group['library']['titles'][0]['title']
|
||||
movie_name = getTitle(group['library'])
|
||||
|
||||
url = self.urls['api'] % self.movieUrlName(movie_name)
|
||||
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url)
|
||||
@@ -44,7 +44,7 @@ class HDTrailers(TrailerProvider):
|
||||
def findViaAlternative(self, group):
|
||||
results = {'480p':[], '720p':[], '1080p':[]}
|
||||
|
||||
movie_name = group['library']['titles'][0]['title']
|
||||
movie_name = getTitle(group['library'])
|
||||
|
||||
url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name}))
|
||||
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
|
||||
|
||||
@@ -204,7 +204,7 @@ class Settings(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
db.close()
|
||||
#db.close()
|
||||
return prop
|
||||
|
||||
def setProperty(self, identifier, value = ''):
|
||||
@@ -221,4 +221,4 @@ class Settings(object):
|
||||
p.value = toUnicode(value)
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
#db.close()
|
||||
|
||||
@@ -12,7 +12,6 @@ class Env(object):
|
||||
|
||||
''' Environment variables '''
|
||||
_encoding = ''
|
||||
_uses_git = False
|
||||
_debug = False
|
||||
_dev = False
|
||||
_settings = Settings()
|
||||
@@ -66,7 +65,7 @@ class Env(object):
|
||||
|
||||
@staticmethod
|
||||
def getEngine():
|
||||
return create_engine(Env.get('db_path'), echo = False)
|
||||
return create_engine(Env.get('db_path'), echo = False, pool_recycle = 30)
|
||||
|
||||
@staticmethod
|
||||
def setting(attr, section = 'core', value = None, default = '', type = None):
|
||||
|
||||
@@ -28,8 +28,6 @@ def getOptions(base_path, args):
|
||||
dest = 'console_log', help = "Log to console")
|
||||
parser.add_argument('--quiet', action = 'store_true',
|
||||
dest = 'quiet', help = 'No console logging')
|
||||
parser.add_argument('--nogit', action = 'store_true',
|
||||
dest = 'nogit', help = 'No git available')
|
||||
parser.add_argument('--daemon', action = 'store_true',
|
||||
dest = 'daemon', help = 'Daemonize the app')
|
||||
parser.add_argument('--pid_file', default = os.path.join(data_dir, 'couchpotato.pid'),
|
||||
@@ -93,7 +91,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
# Register environment settings
|
||||
Env.set('encoding', encoding)
|
||||
Env.set('uses_git', not options.nogit)
|
||||
Env.set('app_dir', base_path)
|
||||
Env.set('data_dir', data_dir)
|
||||
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
|
||||
@@ -132,7 +129,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
# Logger
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S')
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
logger.setLevel(level)
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ from elixir.statements import Statement
|
||||
from elixir.collection import EntityCollection, GlobalEntityCollection
|
||||
|
||||
|
||||
__version__ = '0.7.1'
|
||||
__version__ = '0.8.0dev'
|
||||
|
||||
__all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection',
|
||||
'entities',
|
||||
@@ -85,11 +85,6 @@ def drop_all(*args, **kwargs):
|
||||
def setup_all(create_tables=False, *args, **kwargs):
|
||||
'''Setup the table and mapper of all entities in the default entity
|
||||
collection.
|
||||
|
||||
This is called automatically if any entity of the collection is configured
|
||||
with the `autosetup` option and it is first accessed,
|
||||
instanciated (called) or the create_all method of a metadata containing
|
||||
tables from any of those entities is called.
|
||||
'''
|
||||
setup_entities(entities)
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ Default entity collection implementation
|
||||
import sys
|
||||
import re
|
||||
|
||||
from elixir.py23compat import rsplit
|
||||
|
||||
class BaseCollection(list):
|
||||
def __init__(self, entities=None):
|
||||
list.__init__(self)
|
||||
@@ -24,7 +22,7 @@ class BaseCollection(list):
|
||||
root = entity._descriptor.resolve_root
|
||||
if root:
|
||||
full_path = '%s.%s' % (root, full_path)
|
||||
module_path, classname = rsplit(full_path, '.', 1)
|
||||
module_path, classname = full_path.rsplit('.', 1)
|
||||
module = sys.modules[module_path]
|
||||
res = getattr(module, classname, None)
|
||||
if res is None:
|
||||
|
||||
@@ -3,8 +3,6 @@ This module provides the ``Entity`` base class, as well as its metaclass
|
||||
``EntityMeta``.
|
||||
'''
|
||||
|
||||
from py23compat import sorted
|
||||
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
@@ -25,11 +23,6 @@ from elixir import options
|
||||
from elixir.properties import Property
|
||||
|
||||
DEBUG = False
|
||||
try:
|
||||
from sqlalchemy.orm import EXT_PASS
|
||||
SA05orlater = False
|
||||
except ImportError:
|
||||
SA05orlater = True
|
||||
|
||||
__doc_all__ = ['Entity', 'EntityMeta']
|
||||
|
||||
@@ -205,6 +198,7 @@ class EntityDescriptor(object):
|
||||
parent_desc = self.parent._descriptor
|
||||
tablename = parent_desc.table_fullname
|
||||
join_clauses = []
|
||||
fk_columns = []
|
||||
for pk_col in parent_desc.primary_keys:
|
||||
colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \
|
||||
{'entity': self.parent.__name__.lower(),
|
||||
@@ -214,12 +208,14 @@ class EntityDescriptor(object):
|
||||
# a real column object when said column is not yet
|
||||
# attached to a table
|
||||
pk_col_name = "%s.%s" % (tablename, pk_col.key)
|
||||
fk = ForeignKey(pk_col_name, ondelete='cascade')
|
||||
col = Column(colname, pk_col.type, fk,
|
||||
primary_key=True)
|
||||
col = Column(colname, pk_col.type, primary_key=True)
|
||||
fk_columns.append(col)
|
||||
self.add_column(col)
|
||||
join_clauses.append(col == pk_col)
|
||||
self.join_condition = and_(*join_clauses)
|
||||
self.add_constraint(
|
||||
ForeignKeyConstraint(fk_columns,
|
||||
parent_desc.primary_keys, ondelete='CASCADE'))
|
||||
elif self.inheritance == 'concrete':
|
||||
# Copy primary key columns from the parent.
|
||||
for col in self.parent._descriptor.columns:
|
||||
@@ -286,7 +282,7 @@ class EntityDescriptor(object):
|
||||
self.add_constraint(
|
||||
ForeignKeyConstraint(
|
||||
[e.parent.key for e in con.elements],
|
||||
[e._get_colspec() for e in con.elements],
|
||||
[e.target_fullname for e in con.elements],
|
||||
name=con.name, #TODO: modify it
|
||||
onupdate=con.onupdate, ondelete=con.ondelete,
|
||||
use_alter=con.use_alter))
|
||||
@@ -370,6 +366,11 @@ class EntityDescriptor(object):
|
||||
|
||||
order = []
|
||||
for colname in order_by:
|
||||
#FIXME: get_column uses self.columns[key] instead of property
|
||||
# names. self.columns correspond to the columns of the table if
|
||||
# the table was already created and to self._columns otherwise,
|
||||
# which is a ColumnCollection indexed on columns.key
|
||||
# See ticket #108.
|
||||
col = self.get_column(colname.strip('-'))
|
||||
if colname.startswith('-'):
|
||||
col = desc(col)
|
||||
@@ -493,17 +494,13 @@ class EntityDescriptor(object):
|
||||
(col.key, self.entity.__name__))
|
||||
else:
|
||||
del self._columns[col.key]
|
||||
# are indexed on col.key
|
||||
self._columns.add(col)
|
||||
|
||||
if col.primary_key:
|
||||
self.has_pk = True
|
||||
|
||||
# Autosetup triggers shouldn't be active anymore at this point, so we
|
||||
# can theoretically access the entity's table safely. But the problem
|
||||
# is that if, for some reason, the trigger removal phase didn't
|
||||
# happen, we'll get an infinite loop. So we just make sure we don't
|
||||
# get one in any case.
|
||||
table = type.__getattribute__(self.entity, 'table')
|
||||
table = self.entity.table
|
||||
if table is not None:
|
||||
if check_duplicate and col.key in table.columns.keys():
|
||||
raise Exception("Column '%s' already exist in table '%s' ! " %
|
||||
@@ -595,6 +592,7 @@ class EntityDescriptor(object):
|
||||
#------------------------
|
||||
# some useful properties
|
||||
|
||||
@property
|
||||
def table_fullname(self):
|
||||
'''
|
||||
Complete name of the table for the related entity.
|
||||
@@ -605,8 +603,8 @@ class EntityDescriptor(object):
|
||||
return "%s.%s" % (schema, self.tablename)
|
||||
else:
|
||||
return self.tablename
|
||||
table_fullname = property(table_fullname)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
if self.entity.table is not None:
|
||||
return self.entity.table.columns
|
||||
@@ -615,8 +613,8 @@ class EntityDescriptor(object):
|
||||
# return the parent entity's columns (for example for order_by
|
||||
# using a column defined in the parent.
|
||||
return self._columns
|
||||
columns = property(columns)
|
||||
|
||||
@property
|
||||
def primary_keys(self):
|
||||
"""
|
||||
Returns the list of primary key columns of the entity.
|
||||
@@ -630,15 +628,15 @@ class EntityDescriptor(object):
|
||||
return self.parent._descriptor.primary_keys
|
||||
else:
|
||||
return [col for col in self.columns if col.primary_key]
|
||||
primary_keys = property(primary_keys)
|
||||
|
||||
@property
|
||||
def table(self):
|
||||
if self.entity.table is not None:
|
||||
return self.entity.table
|
||||
else:
|
||||
return FakeTable(self)
|
||||
table = property(table)
|
||||
|
||||
@property
|
||||
def primary_key_properties(self):
|
||||
"""
|
||||
Returns the list of (mapper) properties corresponding to the primary
|
||||
@@ -653,30 +651,32 @@ class EntityDescriptor(object):
|
||||
for prop in mapper.iterate_properties:
|
||||
if isinstance(prop, ColumnProperty):
|
||||
for col in prop.columns:
|
||||
#XXX: Why is this extra loop necessary? What is this
|
||||
# "proxy_set" supposed to mean?
|
||||
for col in col.proxy_set:
|
||||
col_to_prop[col] = prop
|
||||
pk_cols = [c for c in mapper.mapped_table.c if c.primary_key]
|
||||
self._pk_props = [col_to_prop[c] for c in pk_cols]
|
||||
return self._pk_props
|
||||
primary_key_properties = property(primary_key_properties)
|
||||
|
||||
class FakePK(object):
|
||||
def __init__(self, descriptor):
|
||||
self.descriptor = descriptor
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return self.descriptor.primary_keys
|
||||
columns = property(columns)
|
||||
|
||||
class FakeTable(object):
|
||||
def __init__(self, descriptor):
|
||||
self.descriptor = descriptor
|
||||
self.primary_key = FakePK(descriptor)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return self.descriptor.columns
|
||||
columns = property(columns)
|
||||
|
||||
@property
|
||||
def fullname(self):
|
||||
'''
|
||||
Complete name of the table for the related entity.
|
||||
@@ -687,46 +687,8 @@ class FakeTable(object):
|
||||
return "%s.%s" % (schema, self.descriptor.tablename)
|
||||
else:
|
||||
return self.descriptor.tablename
|
||||
fullname = property(fullname)
|
||||
|
||||
|
||||
class TriggerProxy(object):
|
||||
"""
|
||||
A class that serves as a "trigger" ; accessing its attributes runs
|
||||
the setup_all function.
|
||||
|
||||
Note that the `setup_all` is called on each access of the attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, class_, attrname):
|
||||
self.class_ = class_
|
||||
self.attrname = attrname
|
||||
|
||||
def __getattr__(self, name):
|
||||
elixir.setup_all()
|
||||
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
|
||||
#remove the triggers for this entity. This can happen if the entity is
|
||||
#not in the `entities` list for some reason.
|
||||
proxied_attr = getattr(self.class_, self.attrname)
|
||||
return getattr(proxied_attr, name)
|
||||
|
||||
def __repr__(self):
|
||||
proxied_attr = getattr(self.class_, self.attrname)
|
||||
return "<TriggerProxy (%s)>" % (self.class_.__name__)
|
||||
|
||||
|
||||
class TriggerAttribute(object):
|
||||
|
||||
def __init__(self, attrname):
|
||||
self.attrname = attrname
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
|
||||
#remove the triggers for this entity. This can happen if the entity is
|
||||
#not in the `entities` list for some reason.
|
||||
elixir.setup_all()
|
||||
return getattr(owner, self.attrname)
|
||||
|
||||
def is_entity(cls):
|
||||
"""
|
||||
Scan the bases classes of `cls` to see if any is an instance of
|
||||
@@ -805,13 +767,6 @@ def instrument_class(cls):
|
||||
# setup misc options here (like tablename etc.)
|
||||
desc.setup_options()
|
||||
|
||||
# create trigger proxies
|
||||
# TODO: support entity_name... It makes sense only for autoloaded
|
||||
# tables for now, and would make more sense if we support "external"
|
||||
# tables
|
||||
if desc.autosetup:
|
||||
_install_autosetup_triggers(cls)
|
||||
|
||||
|
||||
class EntityMeta(type):
|
||||
"""
|
||||
@@ -823,11 +778,6 @@ class EntityMeta(type):
|
||||
def __init__(cls, name, bases, dict_):
|
||||
instrument_class(cls)
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls._descriptor.autosetup and not hasattr(cls, '_setup_done'):
|
||||
elixir.setup_all()
|
||||
return type.__call__(cls, *args, **kwargs)
|
||||
|
||||
def __setattr__(cls, key, value):
|
||||
if isinstance(value, Property):
|
||||
if hasattr(cls, '_setup_done'):
|
||||
@@ -839,84 +789,6 @@ class EntityMeta(type):
|
||||
type.__setattr__(cls, key, value)
|
||||
|
||||
|
||||
def _install_autosetup_triggers(cls, entity_name=None):
|
||||
#TODO: move as much as possible of those "_private" values to the
|
||||
# descriptor, so that we don't mess the initial class.
|
||||
warnings.warn("The 'autosetup' option on entities is deprecated. "
|
||||
"Please call setup_all() manually after all your entities have been "
|
||||
"declared.", DeprecationWarning, stacklevel=4)
|
||||
tablename = cls._descriptor.tablename
|
||||
schema = cls._descriptor.table_options.get('schema', None)
|
||||
cls._table_key = sqlalchemy.schema._get_table_key(tablename, schema)
|
||||
|
||||
table_proxy = TriggerProxy(cls, 'table')
|
||||
|
||||
md = cls._descriptor.metadata
|
||||
md.tables[cls._table_key] = table_proxy
|
||||
|
||||
# We need to monkeypatch the metadata's table iterator method because
|
||||
# otherwise it doesn't work if the setup is triggered by the
|
||||
# metadata.create_all().
|
||||
# This is because ManyToMany relationships add tables AFTER the list
|
||||
# of tables that are going to be created is "computed"
|
||||
# (metadata.tables.values()).
|
||||
# see:
|
||||
# - table_iterator method in MetaData class in sqlalchemy/schema.py
|
||||
# - visit_metadata method in sqlalchemy/ansisql.py
|
||||
if SA05orlater:
|
||||
warnings.warn(
|
||||
"The automatic setup via metadata.create_all() through "
|
||||
"the autosetup option doesn't work with SQLAlchemy 0.5 and later!")
|
||||
else:
|
||||
# SA 0.6 does not use table_iterator anymore (it was already deprecated
|
||||
# since SA 0.5.0)
|
||||
original_table_iterator = md.table_iterator
|
||||
if not hasattr(original_table_iterator,
|
||||
'_non_elixir_patched_iterator'):
|
||||
def table_iterator(*args, **kwargs):
|
||||
elixir.setup_all()
|
||||
return original_table_iterator(*args, **kwargs)
|
||||
table_iterator.__doc__ = original_table_iterator.__doc__
|
||||
table_iterator._non_elixir_patched_iterator = \
|
||||
original_table_iterator
|
||||
md.table_iterator = table_iterator
|
||||
|
||||
#TODO: we might want to add all columns that will be available as
|
||||
#attributes on the class itself (in SA 0.4+). This is a pretty
|
||||
#rare usecase, as people will normally hit the query attribute before the
|
||||
#column attributes, but I've seen people hitting this problem...
|
||||
for name in ('c', 'table', 'mapper', 'query'):
|
||||
setattr(cls, name, TriggerAttribute(name))
|
||||
|
||||
cls._has_triggers = True
|
||||
|
||||
|
||||
def _cleanup_autosetup_triggers(cls):
|
||||
if not hasattr(cls, '_has_triggers'):
|
||||
return
|
||||
|
||||
for name in ('table', 'mapper'):
|
||||
setattr(cls, name, None)
|
||||
|
||||
for name in ('c', 'query'):
|
||||
delattr(cls, name)
|
||||
|
||||
desc = cls._descriptor
|
||||
md = desc.metadata
|
||||
|
||||
# the fake table could have already been removed (namely in a
|
||||
# single table inheritance scenario)
|
||||
md.tables.pop(cls._table_key, None)
|
||||
|
||||
# restore original table iterator if not done already
|
||||
if not SA05orlater:
|
||||
if hasattr(md.table_iterator, '_non_elixir_patched_iterator'):
|
||||
md.table_iterator = \
|
||||
md.table_iterator._non_elixir_patched_iterator
|
||||
|
||||
del cls._has_triggers
|
||||
|
||||
|
||||
def setup_entities(entities):
|
||||
'''Setup all entities in the list passed as argument'''
|
||||
|
||||
@@ -928,9 +800,6 @@ def setup_entities(entities):
|
||||
if isinstance(attr, Property):
|
||||
delattr(entity, name)
|
||||
|
||||
if entity._descriptor.autosetup:
|
||||
_cleanup_autosetup_triggers(entity)
|
||||
|
||||
for method_name in (
|
||||
'setup_autoload_table', 'create_pk_cols', 'setup_relkeys',
|
||||
'before_table', 'setup_table', 'setup_reltables', 'after_table',
|
||||
@@ -955,8 +824,7 @@ def setup_entities(entities):
|
||||
def cleanup_entities(entities):
|
||||
"""
|
||||
Try to revert back the list of entities passed as argument to the state
|
||||
they had just before their setup phase. It will not work entirely for
|
||||
autosetup entities as we need to remove the autosetup triggers.
|
||||
they had just before their setup phase.
|
||||
|
||||
As of now, this function is *not* functional in that it doesn't revert to
|
||||
the exact same state the entities were before setup. For example, the
|
||||
@@ -968,8 +836,6 @@ def cleanup_entities(entities):
|
||||
"""
|
||||
for entity in entities:
|
||||
desc = entity._descriptor
|
||||
if desc.autosetup:
|
||||
_cleanup_autosetup_triggers(entity)
|
||||
|
||||
if hasattr(entity, '_setup_done'):
|
||||
del entity._setup_done
|
||||
@@ -1007,6 +873,7 @@ class EntityBase(object):
|
||||
for key, value in kwargs.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def update_or_create(cls, data, surrogate=True):
|
||||
pk_props = cls._descriptor.primary_key_properties
|
||||
|
||||
@@ -1016,17 +883,16 @@ class EntityBase(object):
|
||||
record = cls.query.get(pk_tuple)
|
||||
if record is None:
|
||||
if surrogate:
|
||||
raise Exception("cannot create surrogate with pk")
|
||||
raise Exception("Cannot create surrogate with pk")
|
||||
else:
|
||||
record = cls()
|
||||
else:
|
||||
if surrogate:
|
||||
record = cls()
|
||||
else:
|
||||
raise Exception("cannot create non surrogate without pk")
|
||||
raise Exception("Cannot create non surrogate without pk")
|
||||
record.from_dict(data)
|
||||
return record
|
||||
update_or_create = classmethod(update_or_create)
|
||||
|
||||
def from_dict(self, data):
|
||||
"""
|
||||
@@ -1105,10 +971,11 @@ class EntityBase(object):
|
||||
|
||||
# This bunch of session methods, along with all the query methods below
|
||||
# only make sense when using a global/scoped/contextual session.
|
||||
@property
|
||||
def _global_session(self):
|
||||
return self._descriptor.session.registry()
|
||||
_global_session = property(_global_session)
|
||||
|
||||
#FIXME: remove all deprecated methods, possibly all of these
|
||||
def merge(self, *args, **kwargs):
|
||||
return self._global_session.merge(self, *args, **kwargs)
|
||||
|
||||
@@ -1126,6 +993,7 @@ class EntityBase(object):
|
||||
return self._global_session.save_or_update(self, *args, **kwargs)
|
||||
|
||||
# query methods
|
||||
@classmethod
|
||||
def get_by(cls, *args, **kwargs):
|
||||
"""
|
||||
Returns the first instance of this class matching the given criteria.
|
||||
@@ -1133,8 +1001,8 @@ class EntityBase(object):
|
||||
session.query(MyClass).filter_by(...).first()
|
||||
"""
|
||||
return cls.query.filter_by(*args, **kwargs).first()
|
||||
get_by = classmethod(get_by)
|
||||
|
||||
@classmethod
|
||||
def get(cls, *args, **kwargs):
|
||||
"""
|
||||
Return the instance of this class based on the given identifier,
|
||||
@@ -1142,7 +1010,6 @@ class EntityBase(object):
|
||||
session.query(MyClass).get(...)
|
||||
"""
|
||||
return cls.query.get(*args, **kwargs)
|
||||
get = classmethod(get)
|
||||
|
||||
|
||||
class Entity(EntityBase):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from sqlalchemy.orm import reconstructor
|
||||
|
||||
__all__ = [
|
||||
'before_insert',
|
||||
'after_insert',
|
||||
@@ -22,9 +24,4 @@ before_update = create_decorator('before_update')
|
||||
after_update = create_decorator('after_update')
|
||||
before_delete = create_decorator('before_delete')
|
||||
after_delete = create_decorator('after_delete')
|
||||
try:
|
||||
from sqlalchemy.orm import reconstructor
|
||||
except ImportError:
|
||||
def reconstructor(func):
|
||||
raise Exception('The reconstructor method decorator is only '
|
||||
'available with SQLAlchemy 0.5 and later')
|
||||
|
||||
|
||||
@@ -222,12 +222,12 @@ def associable(assoc_entity, plural_name=None, lazy=True):
|
||||
|
||||
# add helper methods
|
||||
def select_by(cls, **kwargs):
|
||||
return cls.query.join([attr_name, 'targets']) \
|
||||
return cls.query.join(attr_name, 'targets') \
|
||||
.filter_by(**kwargs).all()
|
||||
setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
|
||||
|
||||
def select(cls, *args, **kwargs):
|
||||
return cls.query.join([attr_name, 'targets']) \
|
||||
return cls.query.join(attr_name, 'targets') \
|
||||
.filter(*args, **kwargs).all()
|
||||
setattr(entity, 'select_%s' % self.name, classmethod(select))
|
||||
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
'''
|
||||
This extension is DEPRECATED. Please use the orderinglist SQLAlchemy
|
||||
extension instead.
|
||||
|
||||
For details:
|
||||
http://www.sqlalchemy.org/docs/05/reference/ext/orderinglist.html
|
||||
|
||||
For an Elixir example:
|
||||
http://elixir.ematia.de/trac/wiki/Recipes/UsingEntityForOrderedList
|
||||
or
|
||||
http://elixir.ematia.de/trac/browser/elixir/0.7.0/tests/test_o2m.py#L155
|
||||
|
||||
|
||||
|
||||
An ordered-list plugin for Elixir to help you make an entity be able to be
|
||||
managed in a list-like way. Much inspiration comes from the Ruby on Rails
|
||||
acts_as_list plugin, which is currently more full-featured than this plugin.
|
||||
|
||||
Once you flag an entity with an `acts_as_list()` statement, a column will be
|
||||
added to the entity called `position` which will be an integer column that is
|
||||
managed for you by the plugin. You can pass an alternative column name to
|
||||
the plugin using the `column_name` keyword argument.
|
||||
|
||||
In addition, your entity will get a series of new methods attached to it,
|
||||
including:
|
||||
|
||||
+----------------------+------------------------------------------------------+
|
||||
| Method Name | Description |
|
||||
+======================+======================================================+
|
||||
| ``move_lower`` | Move the item lower in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_higher`` | Move the item higher in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to_bottom`` | Move the item to the bottom of the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to_top`` | Move the item to the top of the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to`` | Move the item to a specific position in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
|
||||
|
||||
Sometimes, your entities that represent list items will be a part of different
|
||||
lists. To implement this behavior, simply pass the `acts_as_list` statement a
|
||||
callable that returns a "qualifier" SQLAlchemy expression. This expression will
|
||||
be added to the generated WHERE clauses used by the plugin.
|
||||
|
||||
Example model usage:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from elixir import *
|
||||
from elixir.ext.list import acts_as_list
|
||||
|
||||
class ToDo(Entity):
|
||||
subject = Field(String(128))
|
||||
owner = ManyToOne('Person')
|
||||
|
||||
def qualify(self):
|
||||
return ToDo.owner_id == self.owner_id
|
||||
|
||||
acts_as_list(qualifier=qualify)
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(String(64))
|
||||
todos = OneToMany('ToDo', order_by='position')
|
||||
|
||||
|
||||
The above example can then be used to manage ordered todo lists for people.
|
||||
Note that you must set the `order_by` property on the `Person.todo` relation in
|
||||
order for the relation to respect the ordering. Here is an example of using
|
||||
this model in practice:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
p.todos.append(ToDo(subject='Three'))
|
||||
p.todos.append(ToDo(subject='Two'))
|
||||
p.todos.append(ToDo(subject='One'))
|
||||
session.commit(); session.clear()
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
p.todos[0].move_to_bottom()
|
||||
p.todos[2].move_to_top()
|
||||
session.commit(); session.clear()
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
assert p.todos[0].subject == 'One'
|
||||
assert p.todos[1].subject == 'Two'
|
||||
assert p.todos[2].subject == 'Three'
|
||||
|
||||
|
||||
For more examples, refer to the unit tests for this plugin.
|
||||
'''
|
||||
|
||||
from elixir.statements import Statement
|
||||
from elixir.events import before_insert, before_delete
|
||||
from sqlalchemy import Column, Integer, select, func, literal, and_
|
||||
import warnings
|
||||
|
||||
__all__ = ['acts_as_list']
|
||||
__doc_all__ = []
|
||||
|
||||
|
||||
def get_entity_where(instance):
|
||||
clauses = []
|
||||
for column in instance.table.primary_key.columns:
|
||||
instance_value = getattr(instance, column.name)
|
||||
clauses.append(column == instance_value)
|
||||
return and_(*clauses)
|
||||
|
||||
|
||||
class ListEntityBuilder(object):
|
||||
|
||||
def __init__(self, entity, qualifier=None, column_name='position'):
|
||||
warnings.warn("The act_as_list extension is deprecated. Please use "
|
||||
"SQLAlchemy's orderinglist extension instead",
|
||||
DeprecationWarning, stacklevel=6)
|
||||
self.entity = entity
|
||||
self.qualifier_method = qualifier
|
||||
self.column_name = column_name
|
||||
|
||||
def create_non_pk_cols(self):
|
||||
if self.entity._descriptor.autoload:
|
||||
for c in self.entity.table.c:
|
||||
if c.name == self.column_name:
|
||||
self.position_column = c
|
||||
if not hasattr(self, 'position_column'):
|
||||
raise Exception(
|
||||
"Could not find column '%s' in autoloaded table '%s', "
|
||||
"needed by entity '%s'." % (self.column_name,
|
||||
self.entity.table.name, self.entity.__name__))
|
||||
else:
|
||||
self.position_column = Column(self.column_name, Integer)
|
||||
self.entity._descriptor.add_column(self.position_column)
|
||||
|
||||
def after_table(self):
|
||||
position_column = self.position_column
|
||||
position_column_name = self.column_name
|
||||
|
||||
qualifier_method = self.qualifier_method
|
||||
if not qualifier_method:
|
||||
qualifier_method = lambda self: None
|
||||
|
||||
def _init_position(self):
|
||||
s = select(
|
||||
[(func.max(position_column)+1).label('value')],
|
||||
qualifier_method(self)
|
||||
).union(
|
||||
select([literal(1).label('value')])
|
||||
)
|
||||
a = s.alias()
|
||||
# we use a second func.max to get the maximum between 1 and the
|
||||
# real max position if any exist
|
||||
setattr(self, position_column_name, select([func.max(a.c.value)]))
|
||||
|
||||
# Note that this method could be rewritten more simply like below,
|
||||
# but because this extension is going to be deprecated anyway,
|
||||
# I don't want to risk breaking something I don't want to maintain.
|
||||
# setattr(self, position_column_name, select(
|
||||
# [func.coalesce(func.max(position_column), 0) + 1],
|
||||
# qualifier_method(self)
|
||||
# ))
|
||||
_init_position = before_insert(_init_position)
|
||||
|
||||
def _shift_items(self):
|
||||
self.table.update(
|
||||
and_(
|
||||
position_column > getattr(self, position_column_name),
|
||||
qualifier_method(self)
|
||||
),
|
||||
values={
|
||||
position_column : position_column - 1
|
||||
}
|
||||
).execute()
|
||||
_shift_items = before_delete(_shift_items)
|
||||
|
||||
def move_to_bottom(self):
|
||||
# move the items that were above this item up one
|
||||
self.table.update(
|
||||
and_(
|
||||
position_column >= getattr(self, position_column_name),
|
||||
qualifier_method(self)
|
||||
),
|
||||
values = {
|
||||
position_column : position_column - 1
|
||||
}
|
||||
).execute()
|
||||
|
||||
# move this item to the max position
|
||||
# MySQL does not support the correlated subquery, so we need to
|
||||
# execute the query (through scalar()). See ticket #34.
|
||||
self.table.update(
|
||||
get_entity_where(self),
|
||||
values={
|
||||
position_column : select(
|
||||
[func.max(position_column) + 1],
|
||||
qualifier_method(self)
|
||||
).scalar()
|
||||
}
|
||||
).execute()
|
||||
|
||||
def move_to_top(self):
|
||||
self.move_to(1)
|
||||
|
||||
def move_to(self, position):
|
||||
current_position = getattr(self, position_column_name)
|
||||
|
||||
# determine which direction we're moving
|
||||
if position < current_position:
|
||||
where = and_(
|
||||
position <= position_column,
|
||||
position_column < current_position,
|
||||
qualifier_method(self)
|
||||
)
|
||||
modifier = 1
|
||||
elif position > current_position:
|
||||
where = and_(
|
||||
current_position < position_column,
|
||||
position_column <= position,
|
||||
qualifier_method(self)
|
||||
)
|
||||
modifier = -1
|
||||
|
||||
# shift the items in between the current and new positions
|
||||
self.table.update(where, values = {
|
||||
position_column : position_column + modifier
|
||||
}).execute()
|
||||
|
||||
# update this item's position to the desired position
|
||||
self.table.update(get_entity_where(self)) \
|
||||
.execute(**{position_column_name: position})
|
||||
|
||||
def move_lower(self):
|
||||
# replace for ex.: p.todos.insert(x + 1, p.todos.pop(x))
|
||||
self.move_to(getattr(self, position_column_name) + 1)
|
||||
|
||||
def move_higher(self):
|
||||
self.move_to(getattr(self, position_column_name) - 1)
|
||||
|
||||
|
||||
# attach new methods to entity
|
||||
self.entity._init_position = _init_position
|
||||
self.entity._shift_items = _shift_items
|
||||
self.entity.move_lower = move_lower
|
||||
self.entity.move_higher = move_higher
|
||||
self.entity.move_to_bottom = move_to_bottom
|
||||
self.entity.move_to_top = move_to_top
|
||||
self.entity.move_to = move_to
|
||||
|
||||
|
||||
acts_as_list = Statement(ListEntityBuilder)
|
||||
@@ -124,16 +124,6 @@ The list of supported arguments are as follows:
|
||||
| | module by setting the ``__session__`` attribute of |
|
||||
| | that module. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``autosetup`` | DEPRECATED. Specify whether that entity will contain |
|
||||
| | automatic setup triggers. |
|
||||
| | That is if this entity will be |
|
||||
| | automatically setup (along with all other entities |
|
||||
| | which were already declared) if any of the following |
|
||||
| | condition happen: some of its attributes are accessed |
|
||||
| | ('c', 'table', 'mapper' or 'query'), instanciated |
|
||||
| | (called) or the create_all method of this entity's |
|
||||
| | metadata is called. Defaults to ``False``. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``allowcoloverride``| Specify whether it is allowed to override columns. |
|
||||
| | By default, Elixir forbids you to add a column to an |
|
||||
| | entity's table which already exist in that table. If |
|
||||
@@ -225,7 +215,6 @@ MIGRATION_TO_07_AID = False
|
||||
#
|
||||
options_defaults = dict(
|
||||
abstract=False,
|
||||
autosetup=False,
|
||||
inheritance='single',
|
||||
polymorphic=True,
|
||||
identity=None,
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Some helper functions to get by without Python 2.4
|
||||
|
||||
# set
|
||||
try:
|
||||
set = set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
orig_cmp = cmp
|
||||
# [].sort
|
||||
def sort_list(l, cmp=None, key=None, reverse=False):
|
||||
try:
|
||||
l.sort(cmp, key, reverse)
|
||||
except TypeError, e:
|
||||
if not str(e).startswith('sort expected at most 1 arguments'):
|
||||
raise
|
||||
if cmp is None:
|
||||
cmp = orig_cmp
|
||||
if key is not None:
|
||||
# the cmp=cmp parameter is required to get the original comparator
|
||||
# into the lambda namespace
|
||||
cmp = lambda self, other, cmp=cmp: cmp(key(self), key(other))
|
||||
if reverse:
|
||||
cmp = lambda self, other, cmp=cmp: -cmp(self,other)
|
||||
l.sort(cmp)
|
||||
|
||||
# sorted
|
||||
try:
|
||||
sorted = sorted
|
||||
except NameError:
|
||||
# global name 'sorted' doesn't exist in Python2.3
|
||||
# this provides a poor-man's emulation of the sorted built-in method
|
||||
def sorted(l, cmp=None, key=None, reverse=False):
|
||||
sorted_list = list(l)
|
||||
sort_list(sorted_list, cmp, key, reverse)
|
||||
return sorted_list
|
||||
|
||||
# rsplit
|
||||
try:
|
||||
''.rsplit
|
||||
def rsplit(s, delim, maxsplit):
|
||||
return s.rsplit(delim, maxsplit)
|
||||
|
||||
except AttributeError:
|
||||
def rsplit(s, delim, maxsplit):
|
||||
"""Return a list of the words of the string s, scanning s
|
||||
from the end. To all intents and purposes, the resulting
|
||||
list of words is the same as returned by split(), except
|
||||
when the optional third argument maxsplit is explicitly
|
||||
specified and nonzero. When maxsplit is nonzero, at most
|
||||
maxsplit number of splits - the rightmost ones - occur,
|
||||
and the remainder of the string is returned as the first
|
||||
element of the list (thus, the list will have at most
|
||||
maxsplit+1 elements). New in version 2.4.
|
||||
>>> rsplit('foo.bar.baz', '.', 0)
|
||||
['foo.bar.baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 1)
|
||||
['foo.bar', 'baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 2)
|
||||
['foo', 'bar', 'baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 99)
|
||||
['foo', 'bar', 'baz']
|
||||
"""
|
||||
assert maxsplit >= 0
|
||||
|
||||
if maxsplit == 0: return [s]
|
||||
|
||||
# the following lines perform the function, but inefficiently.
|
||||
# This may be adequate for compatibility purposes
|
||||
items = s.split(delim)
|
||||
if maxsplit < len(items):
|
||||
items[:-maxsplit] = [delim.join(items[:-maxsplit])]
|
||||
return items
|
||||
@@ -278,8 +278,9 @@ relationships accept the following optional (keyword) arguments:
|
||||
| | reference the "local"/current entity's table. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
| ``table`` | Use a manually created table. If this argument is |
|
||||
| | used, Elixir won't generate a table for this |
|
||||
| | relationship, and use the one given instead. |
|
||||
| | used, Elixir will not generate a table for this |
|
||||
| | relationship, and use the one given instead. This |
|
||||
| | argument only accepts SQLAlchemy's Table objects. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
| ``order_by`` | Specify which field(s) should be used to sort the |
|
||||
| | results given by accessing the relation field. |
|
||||
@@ -303,13 +304,6 @@ relationships accept the following optional (keyword) arguments:
|
||||
| ``table_kwargs`` | A dictionary holding any other keyword argument you |
|
||||
| | might want to pass to the underlying Table object. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
| ``column_format`` | DEPRECATED. Specify an alternate format string for |
|
||||
| | naming the |
|
||||
| | columns in the mapping table. The default value is |
|
||||
| | defined in ``elixir.options.M2MCOL_NAMEFORMAT``. You |
|
||||
| | will be passed ``tablename``, ``key``, and ``entity`` |
|
||||
| | as arguments to the format string. |
|
||||
+--------------------+--------------------------------------------------------+
|
||||
|
||||
|
||||
================
|
||||
@@ -493,6 +487,7 @@ class Relationship(Property):
|
||||
self.property = relation(self.target, **kwargs)
|
||||
self.add_mapper_property(self.name, self.property)
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
if not self._target:
|
||||
if isinstance(self.of_kind, basestring):
|
||||
@@ -501,8 +496,8 @@ class Relationship(Property):
|
||||
else:
|
||||
self._target = self.of_kind
|
||||
return self._target
|
||||
target = property(target)
|
||||
|
||||
@property
|
||||
def inverse(self):
|
||||
if not hasattr(self, '_inverse'):
|
||||
if self.inverse_name:
|
||||
@@ -531,7 +526,6 @@ class Relationship(Property):
|
||||
inverse._inverse = self
|
||||
|
||||
return self._inverse
|
||||
inverse = property(inverse)
|
||||
|
||||
def match_type_of(self, other):
|
||||
return False
|
||||
@@ -612,12 +606,12 @@ class ManyToOne(Relationship):
|
||||
def match_type_of(self, other):
|
||||
return isinstance(other, (OneToMany, OneToOne))
|
||||
|
||||
@property
|
||||
def target_table(self):
|
||||
if isinstance(self.target, EntityMeta):
|
||||
return self.target._descriptor.table
|
||||
else:
|
||||
return class_mapper(self.target).local_table
|
||||
target_table = property(target_table)
|
||||
|
||||
def create_keys(self, pk):
|
||||
'''
|
||||
@@ -634,7 +628,10 @@ class ManyToOne(Relationship):
|
||||
source_desc = self.entity._descriptor
|
||||
if isinstance(self.target, EntityMeta):
|
||||
# make sure the target has all its pk set up
|
||||
#FIXME: this is not enough when specifying target_column manually,
|
||||
# on unique, non-pk col, see tests/test_m2o.py:test_non_pk_forward
|
||||
self.target._descriptor.create_pk_cols()
|
||||
|
||||
#XXX: another option, instead of the FakeTable, would be to create an
|
||||
# EntityDescriptor for the SA class.
|
||||
target_table = self.target_table
|
||||
@@ -655,6 +652,12 @@ class ManyToOne(Relationship):
|
||||
"Couldn't find a foreign key constraint in table "
|
||||
"'%s' using the following columns: %s."
|
||||
% (self.entity.table.name, colnames))
|
||||
else:
|
||||
# in this case we let SA handle everything.
|
||||
# XXX: we might want to try to build join clauses anyway so
|
||||
# that we know whether there is an ambiguity or not, and
|
||||
# suggest using colname if there is one
|
||||
pass
|
||||
if self.field:
|
||||
raise NotImplementedError(
|
||||
"'field' argument not allowed on autoloaded table "
|
||||
@@ -808,8 +811,8 @@ class OneToOne(Relationship):
|
||||
# useless because the remote_side is already setup in the other way
|
||||
# (ManyToOne).
|
||||
if self.entity.table is self.target.table:
|
||||
#FIXME: IF this code is of any use, it will probably break for
|
||||
# autoloaded tables
|
||||
# When using a manual/autoloaded table, it will be assigned
|
||||
# an empty list, which doesn't seem to upset SQLAlchemy
|
||||
kwargs['remote_side'] = self.inverse.foreign_key
|
||||
|
||||
# Contrary to ManyToMany relationships, we need to specify the join
|
||||
@@ -839,7 +842,6 @@ class ManyToMany(Relationship):
|
||||
local_colname=None, remote_colname=None,
|
||||
ondelete=None, onupdate=None,
|
||||
table=None, schema=None,
|
||||
column_format=None,
|
||||
filter=None,
|
||||
table_kwargs=None,
|
||||
*args, **kwargs):
|
||||
@@ -858,14 +860,9 @@ class ManyToMany(Relationship):
|
||||
self.table = table
|
||||
self.schema = schema
|
||||
|
||||
if column_format:
|
||||
warnings.warn("The 'column_format' argument on ManyToMany "
|
||||
"relationships is deprecated. Please use the 'local_colname' "
|
||||
"and/or 'remote_colname' arguments if you want custom "
|
||||
"column names for this table only, or modify "
|
||||
"options.M2MCOL_NAMEFORMAT if you want a custom format for "
|
||||
"all ManyToMany tables", DeprecationWarning, stacklevel=3)
|
||||
self.column_format = column_format or options.M2MCOL_NAMEFORMAT
|
||||
#TODO: this can probably be simplified/moved elsewhere since the
|
||||
#argument disappeared
|
||||
self.column_format = options.M2MCOL_NAMEFORMAT
|
||||
if not hasattr(self.column_format, '__call__'):
|
||||
# we need to store the format in a variable so that the
|
||||
# closure of the lambda is correct
|
||||
@@ -891,13 +888,6 @@ class ManyToMany(Relationship):
|
||||
|
||||
super(ManyToMany, self).__init__(of_kind, *args, **kwargs)
|
||||
|
||||
def get_table(self):
|
||||
warnings.warn("The secondary_table attribute on ManyToMany objects is "
|
||||
"deprecated. You should rather use the table attribute.",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return self.table
|
||||
secondary_table = property(get_table)
|
||||
|
||||
def match_type_of(self, other):
|
||||
return isinstance(other, ManyToMany)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user