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