diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index b398d30..29e4bf4 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -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 + diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index 243f621..10a1401 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -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({ diff --git a/couchpotato/core/notifications/history/main.py b/couchpotato/core/notifications/history/main.py index a4ad974..cdd46a4 100644 --- a/couchpotato/core/notifications/history/main.py +++ b/couchpotato/core/notifications/history/main.py @@ -22,6 +22,6 @@ class History(Notification): ) db.add(history) db.commit() - db.close() + #db.close() return True diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index 994da1e..125748f 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -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 diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index bc38857..0d3d09e 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -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 diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 52e6edc..2c5ec73 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -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 diff --git a/couchpotato/core/plugins/profile/main.py b/couchpotato/core/plugins/profile/main.py index 77ad02a..279d650 100644 --- a/couchpotato/core/plugins/profile/main.py +++ b/couchpotato/core/plugins/profile/main.py @@ -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 diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 682866f..fd4f1f6 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -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 = {}): diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 9364a0e..fff5313 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -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 }) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index c60d6cc..24b031e 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -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 = ''): diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 46eccea..9a5f85f 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -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']: diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index 49dbba4..74fed1d 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -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 diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index c27e101..254919f 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -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): diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index 1683972..edf2753 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -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() diff --git a/couchpotato/core/plugins/subtitle/main.py b/couchpotato/core/plugins/subtitle/main.py index 56ff39d..badc78d 100644 --- a/couchpotato/core/plugins/subtitle/main.py +++ b/couchpotato/core/plugins/subtitle/main.py @@ -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): diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index 090a62b..e200afb 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -12,6 +12,9 @@ // ==/UserScript== +if (window.top != window.self) // Only run on top window + return; + var version = {{version}}, host = '{{host}}', api = '{{api}}'; diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py index 9a09edd..d6c9bed 100644 --- a/couchpotato/core/providers/metadata/xbmc/main.py +++ b/couchpotato/core/providers/metadata/xbmc/main.py @@ -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 diff --git a/couchpotato/core/providers/movie/_modifier/main.py b/couchpotato/core/providers/movie/_modifier/main.py index c19af75..60e4c27 100644 --- a/couchpotato/core/providers/movie/_modifier/main.py +++ b/couchpotato/core/providers/movie/_modifier/main.py @@ -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): diff --git a/couchpotato/core/providers/movie/couchpotatoapi/main.py b/couchpotato/core/providers/movie/couchpotatoapi/main.py index 3e438f2..298c06b 100644 --- a/couchpotato/core/providers/movie/couchpotatoapi/main.py +++ b/couchpotato/core/providers/movie/couchpotatoapi/main.py @@ -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) diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index e65a358..d35ca5e 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -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 diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py index a84b556..f66f2f7 100644 --- a/couchpotato/core/providers/movie/themoviedb/main.py +++ b/couchpotato/core/providers/movie/themoviedb/main.py @@ -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 diff --git a/couchpotato/core/providers/nzb/moovee/main.py b/couchpotato/core/providers/nzb/moovee/main.py index a6aeb32..65f118d 100644 --- a/couchpotato/core/providers/nzb/moovee/main.py +++ b/couchpotato/core/providers/nzb/moovee/main.py @@ -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 diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py index 0f7ffb3..7e19d1b 100644 --- a/couchpotato/core/providers/nzb/mysterbin/main.py +++ b/couchpotato/core/providers/nzb/mysterbin/main.py @@ -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()) diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index 2c672f8..40bc2d1 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -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()) diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py index 0b708b2..831ea9c 100644 --- a/couchpotato/core/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/providers/nzb/nzbindex/main.py @@ -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'), diff --git a/couchpotato/core/providers/nzb/x264/main.py b/couchpotato/core/providers/nzb/x264/main.py index d1b9f72..4292dee 100644 --- a/couchpotato/core/providers/nzb/x264/main.py +++ b/couchpotato/core/providers/nzb/x264/main.py @@ -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')) diff --git a/couchpotato/core/providers/torrent/kickasstorrents/main.py b/couchpotato/core/providers/torrent/kickasstorrents/main.py index 6a039c7..0f38604 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/main.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/main.py @@ -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']) diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/providers/trailer/hdtrailers/main.py index 03fa910..b68f76f 100644 --- a/couchpotato/core/providers/trailer/hdtrailers/main.py +++ b/couchpotato/core/providers/trailer/hdtrailers/main.py @@ -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) diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 985515a..c530a27 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -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() diff --git a/couchpotato/environment.py b/couchpotato/environment.py index e42cbad..e804170 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -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): diff --git a/couchpotato/runner.py b/couchpotato/runner.py index e8f2fb0..cf0e5fb 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -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) diff --git a/libs/elixir/__init__.py b/libs/elixir/__init__.py index e92ad7f..a242b53 100644 --- a/libs/elixir/__init__.py +++ b/libs/elixir/__init__.py @@ -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) diff --git a/libs/elixir/collection.py b/libs/elixir/collection.py index 7ba8b66..78127e3 100644 --- a/libs/elixir/collection.py +++ b/libs/elixir/collection.py @@ -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: diff --git a/libs/elixir/entity.py b/libs/elixir/entity.py index 5057457..87f5154 100644 --- a/libs/elixir/entity.py +++ b/libs/elixir/entity.py @@ -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,45 +687,7 @@ 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 "" % (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): """ @@ -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): diff --git a/libs/elixir/events.py b/libs/elixir/events.py index b160928..293a8a4 100644 --- a/libs/elixir/events.py +++ b/libs/elixir/events.py @@ -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') + diff --git a/libs/elixir/ext/associable.py b/libs/elixir/ext/associable.py index 2a37c4b..b31c5a7 100644 --- a/libs/elixir/ext/associable.py +++ b/libs/elixir/ext/associable.py @@ -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)) diff --git a/libs/elixir/ext/list.py b/libs/elixir/ext/list.py deleted file mode 100644 index 3f91c3f..0000000 --- a/libs/elixir/ext/list.py +++ /dev/null @@ -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) diff --git a/libs/elixir/options.py b/libs/elixir/options.py index 9284b04..27d7d19 100644 --- a/libs/elixir/options.py +++ b/libs/elixir/options.py @@ -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, diff --git a/libs/elixir/py23compat.py b/libs/elixir/py23compat.py deleted file mode 100644 index 0d6b1b8..0000000 --- a/libs/elixir/py23compat.py +++ /dev/null @@ -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 diff --git a/libs/elixir/relationships.py b/libs/elixir/relationships.py index f825120..6c14dbb 100644 --- a/libs/elixir/relationships.py +++ b/libs/elixir/relationships.py @@ -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)