1# -*- coding: UTF-8 -*- 2# vim: fdm=marker 3__revision__ = '$Id$' 4 5# Copyright © 2009-2011 Piotr Ożarowski 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU Library General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 20 21# You may use and distribute this software under the terms of the 22# GNU General Public License, version 2 or later 23 24import logging 25import re 26import string 27 28from sqlalchemy import and_, func 29from sqlalchemy.orm import validates, object_session 30from sqlalchemy.sql import select, update 31 32from . import tables 33from . import validators 34 35log = logging.getLogger('Griffith') 36 37EMAIL_PATTERN = re.compile('^[a-z0-9]+[.a-z0-9_+-]*@[a-z0-9_-]+(\.[a-z0-9_-]+)+$', re.IGNORECASE) 38 39 40class DBTable(object): 41 42 __sa_instrumentation_manager__ = validators.InstallValidatorListeners 43 44 def __init__(self, **kwargs): 45 for i in kwargs: 46 if hasattr(self, i): 47 setattr(self, i, kwargs[i]) 48 else: 49 log.warn("%s.%s not set", self.__class__.__name__, i) 50 51 def __repr__(self): 52 return "<%s:%s>" % (self.__class__.__name__, self.name.encode('utf-8')) 53 54 @validates('name') 55 def _validate_name(self, key, name): 56 if not name or not name.strip(): 57 log.warning("%s: empty name (%s)", self.__class__.__name__, name) 58 raise ValueError(_("Name cannot be empty")) 59 return name.strip() 60 61 62class AChannel(DBTable): 63 pass 64 65 66class ACodec(DBTable): 67 pass 68 69 70class Lang(DBTable): 71 pass 72 73 74class Medium(DBTable): 75 pass 76 77 78class Ratio(DBTable): 79 pass 80 81 82class SubFormat(DBTable): 83 pass 84 85 86class Tag(DBTable): 87 pass 88 89 90class VCodec(DBTable): 91 pass 92 93 94class Filter(DBTable): 95 pass 96 97 98class Collection(DBTable): 99 100 def _set_loaned_flag(self, flag): 101 """Sets loaned flag in current collection and all associated movies. 102 103 :param flag: if True and there are loaned movies in the collection 104 already, exception will be raised (whole collection cannot be 105 loaned if one of the movies is not available). 106 Please also remember to create new entry in loans table later (no 107 need to do that if flag is False). 108 """ 109 110 session = object_session(self) 111 112 if flag: # loaning whole collection 113 loaned_movies = session.execute(select([tables.movies.columns.movie_id])\ 114 .where(and_(tables.movies.columns.collection_id == self.collection_id,\ 115 tables.movies.columns.loaned == True))).fetchall() 116 if loaned_movies: 117 log.error('cannot loan it, collection contains loaned movie(s): %s', loaned_movies) 118 raise Exception('loaned movies in the collection already') 119 120 self._loaned = flag 121 update_query = update(tables.movies, tables.movies.columns.collection_id == self.collection_id) 122 session.execute(update_query, params={'loaned': flag}) 123 124 def _is_loaned(self): 125 return self._loaned 126 127 loaned = property(_is_loaned, _set_loaned_flag) 128 129 130class Volume(DBTable): 131 132 def _set_loaned_flag(self, flag): 133 """Sets loaned flag in current volume and all associated movies. 134 135 :param flag: if True, remember to create new entry in loans table 136 later! 137 """ 138 139 session = object_session(self) 140 141 self._loaned = flag 142 update_query = update(tables.movies, tables.movies.columns.volume_id == self.volume_id) 143 session.execute(update_query, params={'loaned': flag}) 144 145 def _is_loaned(self): 146 return self._loaned 147 148 loaned = property(_is_loaned, _set_loaned_flag) 149 150 151class Loan(object): 152 153 def __repr__(self): 154 return "<Loan:%s (person:%s, movie_id:%s, volume_id:%s, collection_id:%s )>" % \ 155 (self.loan_id, self.person_id, self.movie_id, self.volume_id, self.collection_id) 156 157 def returned_on(self, date=None): 158 """ 159 Marks the loan as returned and clears loaned flag in related movies. 160 """ 161 162 if date is None: 163 date = func.current_date() 164 # note that SQLAlchemy will convert YYYYMMDD strings to datetime, no need to touch it 165 166 if self.return_date: # already returned, just update the date 167 self.return_date = date 168 return True 169 170 session = object_session(self) 171 172 if self.collection_id: 173 self.collection.loaned = False # will update the loaned flag in all associated movies as well 174 if self.volume_id: 175 self.volume.loaned = False # will update the loaned flag in all associated movies as well 176 if self.movie_id: 177 self.movie.loaned = False 178 self.return_date = date 179 180 181class Person(DBTable): 182 183 @validates('email') 184 def _validate_email(self, key, address): 185 address = address.strip() 186 if address and not EMAIL_PATTERN.match(address): 187 log.warning("%s: email address is not valid (%s)", self.__class__.__name__, address) 188 raise ValueError(_("E-mail address is not valid")) 189 return address 190 191 @validates('phone') 192 def _digits_only(self, key, value): 193 """removes non-digits""" 194 newvalue = '' 195 for c in value: 196 if c in "0123456789": 197 newvalue += c 198 return newvalue 199 200class Poster(object): 201 202 @validates('md5sum') 203 def _check_md5sum_length(self, key, value): 204 if len(value) != 32: 205 raise ValueError('md5sum has wrong size') 206 return value 207 208 def __init__(self, md5sum=None, data=None): 209 if md5sum and data: 210 self.md5sum = md5sum 211 self.data = data 212 213 def __repr__(self): 214 return "<Poster:%s>" % self.md5sum 215 216 217class Configuration(object): 218 219 def __repr__(self): 220 return "<Config:%s=%s>" % (self.param, self.value) 221 222 223class MovieLang(object): 224 225 def __init__(self, lang_id=None, type=None, acodec_id=None, achannel_id=None, subformat_id=None): 226 self.lang_id = lang_id 227 self.type = type 228 self.acodec_id = acodec_id 229 self.achannel_id = achannel_id 230 self.subformat_id = subformat_id 231 232 def __repr__(self): 233 return "<MovieLang:%s-%s (Type:%s ACodec:%s AChannel:%s SubFormat:%s)>" % \ 234 (self.movie_id, self.lang_id, self.type, self.acodec_id, self.achannel_id, self.subformat_id) 235 236 237class MovieTag(object): 238 239 def __init__(self, tag_id=None): 240 self.tag_id = tag_id 241 242 def __repr__(self): 243 return "<MovieTag:%s-%s>" % (self.movie_id, self.tag_id) 244 245 246# has to be at the end of file (objects from this module are imported there) 247from ._movie import Movie # from _objects import * should import Movie as well 248