def today():
    return date.today() DATE_RANGE_MAX = date(year+1, 7, 5) class UserType(Enum):
    """Enum regarding the type of a user."""
    STUDENT = 1
    TEACHER = 2
    ADMIN = 3
    CLUB_ADMIN = 4
    CLASSROOM_ADMIN = 5
    DIRECTOR = 6

    @property
    def format_name(self):
        """Formats the value of the enum for display."""
        return ['', 'Student', 'Teacher', 'Admin',
                'Club Administrator',
                'Classroom Reservation Administrator',
                'Director'][self.value] class ActivityTime(Enum):
    """Enum regarding the time of an activity."""
    UNKNOWN = 0
    NOON = 1
    AFTERSCHOOL = 2
    HONGMEI = 3
    OTHERS = 4

    @property
    def format_name(self):
        """Formats the value of the enum for display."""
        return ['N/A', 'Lunch', 'Afterschool', 'HongMei',
                'Other'][self.value] class ResStatus(Enum):
    """Enum for status of reservations."""
    UNPAIRED = 1
    PAIRED = 2
    TEACHER = 3 def get_callsign_decorator(objtype, kw):
    '''Decorator function'''
    def decorator(func):
        @wraps(func)
        def decorated_function(*args, **kwargs):
            try:
                item = int(re.match(r'^\d+', kwargs[kw]).group(0))
                item = objtype(item)
                item._data
            except (NameError, AttributeError, OverflowError, NoRow):
                abort(404)
            kwargs[kw] = item
            setattr(g, kw, item)
            return func(*args, **kwargs)

        return decorated_function

    return decorator def special_access_required(func):
    @login_required
    @wraps(func)
    def decorated_function(*args, **kwargs):
        if current_user.type != UserType.ADMIN \
                and current_user.type != UserType.CLASSROOM_ADMIN \
                and current_user.type != UserType.CLUB_ADMIN \
                and current_user.type != UserType.DIRECTOR:
            if 'club' in kwargs:
                club = kwargs['club']
            elif 'activity' in kwargs:
                club = kwargs['activity'].club
            else:
                abort(403)  # Admin-only page

            if current_user.type == UserType.TEACHER:
                if current_user != club.teacher:
                    abort(403)
            else:
                if current_user != club.leader:
                    abort(403)
        return func(*args, **kwargs)

    return decorated_function class Pagination(object):
    def __init__(self, page, per_page, total_count):
        self.page = page
        self.per_page = per_page
        self.total_count = total_count

    @property
    def pages(self):
        return int(ceil(self.total_count / float(self.per_page)))

    @property
    def has_prev(self):
        return self.page > 1

    @property
    def has_next(self):
        return self.page < self.pages

    def iter_pages(self, left_edge=2, left_current=3,
                   right_current=4, right_edge=2):
        last = 0
        for num in xrange(1, self.pages + 1):
            if num <= left_edge or \
               (num > self.page - left_current - 1 and
                num < self.page + right_current) or \
               num > self.pages - right_edge:
                if last + 1 != num:
                    yield {'num': None}
                if num == 1 or num == self.pages or abs(self.page - num) < 2:
                    yield {'num': num, 'hide': 0}  # Do not hide in phone
                elif abs(self.page - num) < 3:
                    yield {'num': num, 'hide': 1}  # Hide in phone secondly
                else:
                    yield {'num': num, 'hide': 2}  # Hide in phone firstly
                last = num def render_email_template(name, parameters):
    with open(os.path.join('/srv/oclubs/oclubs/email_templates/',
                           name + '.mustache'), 'r') as textfile:
        data = textfile.read()

    return pystache.render(data, parameters) def download_xlsx(filename, info):
    '''Çreate xlsx file for given info and download it '''
    output = StringIO()
    workbook = xlsxwriter.Workbook(output, {'in_memory': True})
    worksheet = workbook.add_worksheet()
    row_num = 0
    col_num = 0

    for row in info:
        for grid in row:
            worksheet.write(row_num, col_num, grid)
            col_num += 1
        col_num = 0
        row_num += 1
    workbook.close()
    output.seek(0)
    headers = Headers()
    headers.set('Content-Disposition', 'attachment', filename=filename)
    return Response(output.read(), mimetype='application/vnd.openxmlformats-'
                    'officedocument.spreadsheetml.sheet', headers=headers) def partition(p, l):
    return reduce(lambda x, y: x[not p(y)].append(y) or x, l, ([], [])) def require_student_membership(func):
    @login_required
    @wraps(func)
    def decorated_function(*args, **kwargs):
        if 'club' in kwargs:
            club = kwargs['club']
        elif 'activity' in kwargs:
            club = kwargs['activity'].club
        else:
            assert False

        if current_user not in club.members:
            abort(403)

        return func(*args, **kwargs)

    return decorated_function def require_past_activity(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        activity = kwargs['activity']

        if activity.is_future:
            abort(403)

        return func(*args, **kwargs)

    return decorated_function def require_future_activity(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        activity = kwargs['activity']

        if not activity.is_future:
            abort(403)

        return func(*args, **kwargs)

    return decorated_function def require_active_club(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        if 'club' in kwargs:
            club = kwargs['club']
        elif 'activity' in kwargs:
            club = kwargs['activity'].club
        else:
            assert False

        if not club.is_active:
            abort(403)

        return func(*args, **kwargs)

    return decorated_function def true_or_fail(cond, msg, group):
    if not cond:
        fail(msg, group) def form_is_valid():
    return not g.get('hasfailures', False) def error_or_fail(cond, exc, msg, group):
    try:
        cond()
    except exc:
        pass
    else:
        fail(msg, group) def fail(msg, group):
    flash(msg, group)
    g.hasfailures = True def get_callsign(objtype, kw):
    '''Function that returns the object directly'''
    try:
        item = int(re.match(r'^\d+', kw).group(0))
        item = objtype(item)
    except (NameError, AttributeError, OverflowError, NoRow):
        abort(404)
    return item class FormattedText(BaseObject):
    table = 'text'
    identifier = 'text_id'
    club = Property('text_club', 'Club')
    uploader = Property('text_user', 'User')
    _blob = Property('text_data')
    _flags = Property('text_flags',
                      (lambda x: x.split(','), lambda x: ','.join(x)))

    _emptytext = None

    def __init__(self):
        super(FormattedText, self).__init__()
        self._raw = self._formatted = None

    @property
    def id(self):
        return self._id

    @property
    def raw(self):
        if self.is_real:
            if self._raw is None:
                if 'external' in self._flags:
                    # TODO
                    pass
                else:
                    self._raw = self._blob.decode('utf-8')
            return self._raw
        else:
            return ''

    @property
    def formatted(self):
        if self._formatted is None:
            self._formatted = Markup(self.format(self.raw))
        return self._formatted

    @classmethod
    def handle(cls, user, club, text):
        if not text or not text.strip():
            return cls.emptytext()
        obj = cls.new()
        obj.club = club
        obj.uploader = user
        obj._flags = []
        obj._blob = text.strip().encode('utf-8')
        return obj.create()

    @staticmethod
    def format(rawstr):
        return bleach.clean(
            markdown(
                rawstr,
                output_format='html5',
                lazy_ol=False
            ),
            tags=ALLOWED_TAGS
        )

    @classmethod
    def emptytext(cls):
        if cls._emptytext is None:
            cls._emptytext = cls(0)
        return cls._emptytext class Activity(BaseObject):
    table = 'activity'
    identifier = 'act_id'
    name = Property('act_name', search=True)
    club = Property('act_club', 'Club')
    description = Property('act_desc', 'FormattedText', search=True)
    date = Property('act_date', (int_to_dateobj, dateobj_to_int))
    time = Property('act_time', ActivityTime)
    location = Property('act_location')
    cas = Property('act_cas', (lambda val: val/60, lambda val: val*60))
    post = Property('act_post', 'FormattedText', search=True)
    selections = Property('act_selections', json, error_default='[]')
    reservation = Property('act_reservation', 'Reservation')
    attendance = ListProperty('attendance', 'att_act', 'att_user', 'User')
    pictures = ListProperty('act_pic', 'actpic_act', 'actpic_upload', 'Upload')

    @property
    def is_future(self):
        return self.date > date.today()

    @property
    def has_reservation(self):
        return True if self.reservation is not None else False

    @property
    def one_line_selections(self):
        return ';'.join(self.selections)

    def signup(self, user, **kwargs):
        data = {
            'signup_consentform': ('consentform', False),
            'signup_selection': ('selection', '')
        }

        insert = {'signup_act': self.id, 'signup_user': user.id}
        update = {}

        for dbkey, (kwkey, default) in data.items():
            if kwkey in kwargs:
                insert[dbkey] = kwargs[kwkey]
                update[dbkey] = kwargs[kwkey]
            else:
                insert[dbkey] = default

        database.insert_or_update_row('signup', insert, update)

    def signup_undo(self, user):
        database.delete_rows(
            'signup',
            {'signup_act': self.id, 'signup_user': user.id}
        )

    def signup_list(self):
        from oclubs.objs import User

        ret = database.fetch_multirow(
            'signup',
            {
                'signup_user': 'user',
                'signup_consentform': 'consentform',
                'signup_selection': 'selection'
            },
            {'signup_act': self.id}
        )
        for item in ret:
            item['user'] = User(item['user'])

        return ret

    def signup_user_status(self, user):
        return database.fetch_onerow(
            'signup',
            {
                'signup_consentform': 'consentform',
                'signup_selection': 'selection'
            },
            {
                'signup_act': self.id,
                'signup_user': user.id
            }
        )

    def attend(self, user):
        database.insert_row(
            'attendance',
            {'att_act': self.id, 'att_user': user.id}
        )
        del self.attendance

    def attend_undo(self, user):
        database.delete_rows(
            'attendance',
            {'att_act': self.id, 'att_user': user.id}
        )
        del self.attendance

    def add_picture(self, upload):
        database.insert_row('act_pic',
                            {'actpic_act': self.id,
                             'actpic_upload': upload.id})
        del self.pictures

    def remove_picture(self, upload):
        database.delete_rows('act_pic',
                             {'actpic_act': self.id,
                              'actpic_upload': upload.id})
        del self.pictures

    def delete_activity(self):
        try:
            database.delete_rows('act_pic',
                                {'actpic_act': self.id})
        except:
            pass
        database.delete_rows('activity',{'act_id': self.id})
        del self.pictures

    @classmethod
    @paged_db_read
    def get_activities_conditions(cls, times=(), additional_conds=None,
                                  dates=(True, True), club_types=(),
                                  excellent_only=False, grade_limit=(),
                                  require_photos=False, require_attend=False,
                                  order_by_time=True, pager=None):
        conds = {}
        if additional_conds:
            conds.update(additional_conds)

        conds['where'] = conds.get('where', [])

        if isinstance(dates, date):
            conds['where'].append(('=', 'act_date', dateobj_to_int(dates)))
        elif dates != (True, True):
            start, end = dates

            if start is True:
                conds['where'].append(('<=', 'act_date',
                                       dateobj_to_int(end or date.today())))
            elif end is True:
                conds['where'].append(('>', 'act_date',
                                       dateobj_to_int(start or date.today())))
            else:
                start = (start or date.today()) + ONE_DAY
                end = (end or date.today()) + ONE_DAY
                conds['where'].append(('range', 'act_date',
                                       (dateobj_to_int(start),
                                        dateobj_to_int(end))))

        if times:
            times = [time.value for time in times]
            conds['where'].append(('in', 'act_time', times))

        if club_types or excellent_only or grade_limit:
            conds['join'] = conds.get('join', [])
            conds['join'].append(('inner', 'club', [('club_id', 'act_club')]))
        if club_types:
            club_types = [club_type.value for club_type in club_types]
            conds['where'].append(('in', 'club_type', club_types))
        if excellent_only:
            from oclubs.objs import Club
            conds['where'].append(('in', 'club_id', Club._excellentclubs()))
        if grade_limit:
            conds['join'].append(
                ('inner', 'user', [('user_id', 'club_leader')]))
            conds['where'].append(('in', 'user_grade', grade_limit))

        if require_photos:
            conds['join'] = conds.get('join', [])
            conds['join'].append(
                ('inner', 'act_pic', [('actpic_act', 'act_id')]))
        if require_attend:
            conds['join'] = conds.get('join', [])
            conds['join'].append(
                ('inner', 'attendance', [('att_act', 'act_id')]))

        if order_by_time:
            conds['order'] = conds.get('order', [])
            conds['order'].append(('act_date', False))

        pager_fetch, pager_return = pager

        ret = pager_fetch(database.fetch_onecol,
                          cls.table,
                          cls.identifier,
                          conds,
                          distinct=True)

        ret = [cls(item) for item in ret]

        return pager_return(ret)

    @classmethod
    def all_activities(cls):
        return cls.get_activities_conditions()

    @classmethod
    def thisweek_activities(cls):
        weekday = date.today().weekday()
        today = date.today()
        return cls.get_activities_conditions(
            dates=(today - timedelta(weekday + 1),
                   today + timedelta(6 - weekday))) class User(BaseObject, UserMixin):
    table = 'user'
    identifier = 'user_id'
    studentid = Property('user_login_name')
    gnumber_id = Property('user_gnumber_id')
    short_id = Property('user_short_id')
    passportname = Property('user_passport_name')
    password = Property('user_password', (NotImplemented, _encrypt))
    nickname = Property('user_nick_name', rediscached=True)
    initalized = Property('user_initalized', bool)
    email = Property('user_email')
    phone = Property('user_phone')
    picture = Property('user_picture', 'Upload')
    type = Property('user_type', UserType)
    grade = Property('user_grade', int)
    currentclass = Property('user_class', int)
    clubs = ListProperty('club_member', 'cm_user', 'cm_club', 'Club')
    attendance = ListProperty('attendance', 'att_user', 'att_act', 'Activity')

    GRADECLASSREGEX = re.compile(
        r'^\s*(\d+)(?:\s*[(-_/\\]\s*|\s+)(\d+)\s*(?:\)\s*)?(?:[AB]\s*)?$')

    PREFERENCES = {
        'receive_email': (lambda x: bool(int(x)), True),
    }

    @property
    def callsign(self):
        return str(self.id)

    @property
    def grade_and_class(self):
        # %d cannot accept None
        return '%s(%s)' % (self.grade, self.currentclass)

    @property
    def is_disabled(self):
        return not self._data['password']

    def activities_reminder(self, types, signedup_only=False):
        from oclubs.objs import Activity

        if signedup_only:
            return Activity.get_activities_conditions(
                types,
                {
                    'join': [('inner', 'signup',
                             [('act_id', 'signup_act')])],
                    'where': [('=', 'signup_user', self.id)]
                },
                dates=(False, True),
                order_by_time=True
            )
        else:
            return Activity.get_activities_conditions(
                types,
                {
                    'join': [('inner', 'club_member',
                             [('act_club', 'cm_club')])],
                    'where': [('=', 'cm_user', self.id)]
                },
                dates=(False, True),
                order_by_time=True
            )

    def email_user(self, title, contents):
        if self.is_disabled:
            return

        if self.email and '@' in self.email \
                and self.get_preference('receive_email'):
            email.send((self.email, self.passportname), title, contents)

    def notify_user(self, contents):
        if self.is_disabled:
            return

        database.insert_row(
            'notification',
            {
                'notification_user': self.id,
                'notification_text': contents,
                'notification_isread': False,
                'notification_date': dateobj_to_int(date.today())
            }
        )

    def get_preference(self, name):
        typ, default = self.PREFERENCES[name]

        try:
            ret = database.fetch_oneentry(
                'preferences',
                'pref_value',
                {'pref_user': self.id, 'pref_type': name}
            )
        except NoRow:
            return default
        else:
            return typ(ret)

    def set_preference(self, name, value):
        # we don't set preferences when it is unnecessary,
        # so users can keep a "default" state
        if value == self.get_preference(name):
            return

        update = {'pref_value': value}
        insert = {'pref_user': self.id, 'pref_type': name}
        insert.update(update)
        database.insert_or_update_row('preferences', insert, update)

    @paged_db_read
    def get_notifications(self, pager):
        pager_fetch, pager_return = pager
        ret = pager_fetch(
            database.fetch_multirow,
            'notification',
            {
                'notification_text': 'text',
                'notification_isread': 'isread',
                'notification_date': 'date'
            },
            {
                'where': [('=', 'notification_user', self.id)],
                'order': [('notification_date', False)]
            }
        )
        for item in ret:
            item['date'] = int_to_dateobj(item['date'])

        return pager_return(ret)

    def set_notifications_readall(self):
        try:
            database.update_row(
                'notification',
                {'notification_isread': True},
                {'notification_user': self.id}
            )
        except NoRow:
            pass

    def get_unread_notifications_num(self):
        noti = database.fetch_oneentry(
            'notification',
            database.RawSQL('COUNT(*)'),
            {'notification_user': self.id, 'notification_isread': False}
        )
        invi = database.fetch_oneentry(
            'invitation',
            database.RawSQL('COUNT(*)'),
            {'invitation_user': self.id}
        )
        return noti + invi

    def get_invitation(self):
        from oclubs.objs import Club

        ret = database.fetch_multirow(
                'invitation',
                {'invitation_club': 'club', 'invitation_date': 'date'},
                {'invitation_user': self.id}
            )
        for item in ret:
            item['date'] = int_to_dateobj(item['date'])
            item['club'] = Club(item['club'])
        return ret

    def delete_invitation(self, club):
        try:
            database.delete_rows(
                'invitation',
                {'invitation_club': club.id, 'invitation_user': self.id}
            )
        except NoRow:
            pass

    @classmethod
    def attempt_login(cls, username, password):
        def emptypw():
            # to gave some delay, verify empty password and discard the results
            _crypt.verify(
                password,
                '$2a$12$mf04JOZtIxRtPFw793AGyeYGHGuiN2ikL/HO9fEKdCIilJqwRZKg.'
            )

        if not password:
            emptypw()
            return

        data = database.fetch_multirow(
            'user',
            {'user_id': 'id', 'user_password': 'password'},
            {'user_login_name': username}
        )

        if not data:
            emptypw()
            return

        else:
            for d in data:
                if not d['password']:
                    emptypw()
                    continue
                elif _crypt.verify(password, d['password']):
                    return cls(d['id'])
                else:
                    continue
            else:
                emptypw()
                return

    @classmethod
    def extract_gradeclass(cls, gradeclass):
        reobj = cls.GRADECLASSREGEX.match(gradeclass)
        if not reobj:
            raise ValueError

        return int(reobj.group(1)), int(reobj.group(2))

    @classmethod
    def find_user(cls, gradeclass, gnumber):
        try:
            grade, currentclass = cls.extract_gradeclass(gradeclass)
        except ValueError:
            return

        try:
            return cls(database.fetch_oneentry(
                'user',
                'user_id',
                {
                    'user_grade': grade,
                    'user_class': currentclass,
                    'user_gnumber_id': gnumber
                }
            ))
        except NoRow:
            return

    @classmethod
    def find_teacher(cls, emailaddress):
        try:
            return cls(database.fetch_oneentry(
                'user',
                'user_id',
                {
                    'user_email': emailaddress,
                    'user_type': UserType.TEACHER.value
                }
            ))
        except NoRow:
            # Dynamically create a new teacher
            from oclubs.objs import Upload

            ret = cls.new()
            ret.studentid = emailaddress
            ret.passportname = emailaddress
            ret.gnumber_id = emailaddress
            ret.short_id = emailaddress

            ret.password = None
            ret.nickname = emailaddress
            ret.email = emailaddress
            ret.phone = None
            ret.picture = Upload(-101)
            ret.type = UserType.TEACHER
            ret.grade = None
            ret.currentclass = None
            ret.initalized = False

            return ret.create()

    @classmethod
    def allusers(cls, only_students=False):
        conds = []
        if only_students:
            conds.append(('=', 'user_type', UserType.STUDENT.value))

        tempdata = database.fetch_onecol(
            cls.table,
            cls.identifier,
            conds
        )
        return [cls(item) for item in tempdata]

    @classmethod
    def get_userobj_from_passportname(cls, passportname):
        allusers = cls.allusers()
        for user in allusers:
            if user.passportname == passportname:
                return user

        return None

    @classmethod
    def get_userobj_from_loginname(cls, loginname):
        allusers = cls.allusers()
        for user in allusers:
            if user.studentid == loginname:
                return user

        return None

    @classmethod
    def get_new_passwords(cls):
        return [(
            cls(int(key.split(':')[-1])),
            redis.RedisCache(key, 0).detach().get()
        ) for key in redis.r.keys('tempuserpw:*')]

    @staticmethod
    def generate_password():
        return xp.generate_xkcdpassword(_words)

    @staticmethod
    def new_reset_request(userObj):
        '''Creates a new pending reset'''
        reset_request_id = uuid.uuid4().hex

        reset_request = redis.RedisCache('reset_request:' + reset_request_id,
                                         1800)
        reset_request.set(userObj.callsign)

        return reset_request_id

    @staticmethod
    def get_reset_request(tokenStr):
        '''Return user callsign from reset token'''
        return redis.RedisCache(
            'reset_request:' + tokenStr, 1800).detach().get() class Upload(BaseObject):
    table = 'upload'
    identifier = 'upload_id'
    club = Property('upload_club', 'Club')
    uploader = Property('upload_user', 'User')
    _location = Property('upload_loc')
    mime = Property('upload_mime')

    # Don't use mimetypes.guess_extension(mime) -- 'image/jpeg' => '.jpe'
    _mimedict = {
        'image/jpeg': '.jpg',
        'image/png': '.png',
    }

    @property
    def id(self):
        return self._id

    @property
    def location_local(self):
        if self.is_real:
            return self.mk_internal_path(self._location)
        else:
            return self.mk_internal_path(-self.id, False)

    @property
    def location_external(self):
        if self.is_real:
            return self.mk_external_path(self._location)
        else:
            return self.mk_external_path(-self.id, False)

    @classmethod
    def handle(cls, user, club, file):
        filename = os.urandom(8).encode('hex')
        temppath = os.path.join('/tmp', filename)
        file.save(temppath)

        try:
            # Don't use mimetypes.guess_type(temppath) -- Faked extensions
            mime = magic.from_file(temppath, mime=True)
            if mime not in cls._mimedict:
                raise UploadNotSupported

            filename = filename + cls._mimedict[mime]
            permpath = cls.mk_internal_path(filename)
            permdir = os.path.dirname(permpath)
            if not os.path.isdir(permdir):
                os.makedirs(permdir, 0o755)

            # resize to 600, 450
            cls._thumb(temppath, permpath)
            fs.watch(permpath)
        finally:
            os.remove(temppath)

        obj = cls.new()
        obj.club = club
        obj.uploader = user
        obj._location = filename
        obj.mime = mime
        return obj.create()

    @staticmethod
    def _thumb(temppath, permpath):
        im = Image.open(temppath)

        try:
            # HACK: Fixing EXIF orientation
            exif = dict(im._getexif().items())
            exif[ORIENTATION]
        except Exception:
            pass
        else:
            if exif[ORIENTATION] == 3:
                im = im.rotate(180, expand=True)
            elif exif[ORIENTATION] == 6:
                im = im.rotate(270, expand=True)
            elif exif[ORIENTATION] == 8:
                im = im.rotate(90, expand=True)

        im.thumbnail((4000, 3000))
        im.save(permpath, optimize=True)

    @staticmethod
    def mk_relative_path(filename, is_upload=True):
        if is_upload:
            return os.path.join('images', filename[0], filename[:2], filename)
        else:
            return os.path.join('static/images/icons', 'icon%d.jpg' % filename)

    @staticmethod
    def mk_internal_path(filename, is_upload=True):
        return os.path.join('/srv/oclubs',
                            Upload.mk_relative_path(filename, is_upload))

    @staticmethod
    def mk_external_path(filename, is_upload=True):
        return os.path.join('/', Upload.mk_relative_path(filename, is_upload)) class Reservation(BaseObject):
    table = 'reservation'
    identifier = 'res_id'
    activity = Property('res_activity', 'Activity')  # for club reservations
    # the date for when the reservation is effective
    date = Property('res_date', (int_to_dateobj, dateobj_to_int))
    # the date for when the reservation was created
    date_of_reservation = Property('res_date_of_res',
                                   (int_to_dateobj, dateobj_to_int))
    timeslot = Property('res_timeslot', ActivityTime)
    status = Property('res_status', ResStatus)
    activity_name = Property('res_activity_name')
    reserver_name = Property('res_reserver_name')   # club name, teacher name
    reserver_club = Property('res_reserver_club', 'Club')
    owner = Property('res_owner', 'User')  # user that created the reservation
    classroom = Property('res_classroom', 'Classroom')
    SBNeeded = Property('res_SBNeeded', bool)
    SBAppDesc = Property('res_SBAppDesc')
    instructors_approval = Property('res_instructors_approval', bool)
    directors_approval = Property('res_directors_approval', bool)
    SBApp_status = Property('res_SBApp_status', SBAppStatus)

    @property
    def callsign(self):
        return '-'.join(filter(None, (
            str(self.id),
            self.classroom.location.replace(' ', '_'),
            str(dateobj_to_int(self.date))
        )))

    @classmethod
    @paged_db_read
    def get_reservations_conditions(cls, timeslot=None, additional_conds=None,
                                    dates=(True, True), status=None,
                                    room_buildings=(), reserver_club=None,
                                    room_numbers=(), SBNeeded=None,
                                    instructors_approval=None, owner=None,
                                    directors_approval=None,
                                    SBApp_status=None, order_by_date=True,
                                    pager=None):
        """
        Get reservations

        timeslot: ActivityTime object
        dates type: Date object
        room_building: either one Building object or list of Building objects
        room_number: list of strings

        return: list of Reservation objects
        """

        conds = {}
        if additional_conds:
            conds.update(additional_conds)

        conds['where'] = conds.get('where', [])

        if status is not None:
            conds['where'].append(('=', 'res_status', status.value))

        if SBNeeded is not None:
            conds['where'].append(('=', 'res_SBNeeded', SBNeeded))
        if instructors_approval is not None:
            conds['where'].append(('=', 'res_instructors_approval',
                                   instructors_approval))
        if directors_approval is not None:
            conds['where'].append(('=', 'res_directors_approval',
                                   directors_approval))
        if SBApp_status is not None:
            conds['where'].append(('=', 'res_SBApp_status',
                                   SBApp_status.value))

        if isinstance(dates, date):
            conds['where'].append(('=', 'res_date', dateobj_to_int(dates)))
        elif dates != (True, True):
            start, end = dates
            if start is True:
                conds['where'].append(('<=', 'res_date',
                                       dateobj_to_int(end or date.today())))
            elif end is True:
                conds['where'].append(('>=', 'res_date',
                                       dateobj_to_int(start or date.today())))
            else:
                start = (start or date.today()) + ONE_DAY
                end = (end or date.today()) + ONE_DAY
                conds['where'].append(('range', 'res_date',
                                       (dateobj_to_int(start),
                                        dateobj_to_int(end))))

        if timeslot:
            conds['where'].append(('=', 'res_timeslot', timeslot.value))

        if reserver_club:
            conds['where'].append(('=', 'res_reserver_club', reserver_club.id))

        if owner:
            conds['where'].append(('=', 'res_owner', owner))

        conds['join'] = conds.get('join', [])
        conds['join'].append(('inner', 'classroom',
                             [('room_id', 'res_classroom')]))

        if room_buildings:
            if isinstance(room_buildings, Building):
                conds['where'].append(('in', 'room_building',
                                       [room_buildings.value]))
            else:
                room_buildings = [room_building.value for
                                  room_building in room_buildings]
                conds['where'].append(('in', 'room_building', room_buildings))
        if room_numbers:
            conds['where'].append(('in', 'room_number', room_numbers))

        if order_by_date:
            conds['order'] = conds.get('order', [])
            conds['order'].append(('res_date', False))

        pager_fetch, pager_return = pager

        ret = pager_fetch(database.fetch_onecol,
                          cls.table,
                          cls.identifier,
                          conds, distinct=True)

        ret = [cls(item) for item in ret]

        return pager_return(ret)

    @classmethod
    def delete_reservation(cls, single_date=None, timeslot=None, building=None, room_number=None,
                           owner=None):
        conds = {}

        conds['where'] = conds.get('where', [])
        if single_date:
            conds['where'].append(('=', 'res_date', dateobj_to_int(single_date)))
        if timeslot:
            conds['where'].append(('=', 'res_timeslot', timeslot.value))
        if owner:
            conds['where'].append(('=', 'res_owner', owner.id))
        
        if building:
            conds['join'] = conds.get('join', [])
            conds['join'].append(('inner', 'classroom',
                                [('room_id', 'res_classroom')]))
        if building:
            conds['where'].append(('in', 'room_building',
                                [building.value]))
        if room_number:
            conds['where'].append(('in', 'room_number', [room_number]))

        ret = database.delete_rows(cls.table, conds)

        return ret class UploadNotSupported(Exception):
    """Exception occuring when an uploaded file is not supported."""
    pass class NoRow(Exception):
    """Exception occuring when no row is found from the database."""
    pass class PairReservation(FlaskForm):
    reservations_for_pairing = SelectField(
        'Unpaired reservations',
        choices=[('none', 'None available')])

    submit = SubmitField('Pair Reservation')

    def check(self):
        if self.reservations_for_pairing.data == 'none':
            self.errors[self.reservations_for_pairing] = \
                'No reservations available for pairing.'
            return False

        return True
from datetime import date
from flask import (
    Blueprint, render_template, url_for, request, redirect, flash
)
from flask_login import current_user, login_required, fresh_login_required
from oclubs.utils.dates import today, DATE_RANGE_MAX
from oclubs.enums import UserType, ActivityTime, ResStatus
from oclubs.shared import (
    get_callsign_decorator, special_access_required, Pagination,
    render_email_template, download_xlsx, partition,
    require_student_membership, require_past_activity,
    require_future_activity, require_active_club,
    true_or_fail, form_is_valid, error_or_fail, fail, get_callsign
)
from oclubs.objs import User, Activity, Upload, FormattedText, Reservation
from oclubs.exceptions import UploadNotSupported, NoRow
from oclubs.forms.reservation_forms import PairReservation
def actstatus(activity):
    '''Check Activity Status'''
    return render_template('activity/actstatus.html.j2')


@actblueprint.route('/<activity>/signup_status/submit', methods=['POST'])
@get_callsign_decorator(Activity, 'activity')
@require_active_club
@special_access_required
@require_future_activity
def actstatus_submit(activity):
    '''Change consent form status'''
    member = User(request.form['uid'])
    true_or_fail(member in activity.club.members, member.nickname +
                 ' is not a member of this club.', 'consent_form')

    if form_is_valid():
        status = int(request.form['status'])
        if status == 0:
            activity.signup(member, consentform=True)
            flash(member.nickname + ' has handed in the consent form.',
                  'consent_form')
        elif status == 1:
            activity.signup(member, consentform=False)
            flash(member.nickname + ' has not handed in the consent form.',
                  'consent_form')
    return redirect(url_for('.actstatus', activity=activity.callsign))


@actblueprint.route('/<activity>/signup_status/download')
@get_callsign_decorator(Activity, 'activity')
@special_access_required
def actstatus_download(activity):
    '''Download activity status'''
    info = []
    info.append(['Nick Name', 'Email', 'Phone', 'Consent Form',
                 'Selection' if activity.selections else ''])
    info.extend([(member['user'].nickname,
                  member['user'].email,
                  str(member['user'].phone),
                  'Handed in' if member['consentform'] == 1
                  else 'Not handed in',
                  member['selection'] if activity.selections else '')
                for member in activity.signup_list()])
    return download_xlsx('Activity Status - ' + activity.name + '.xlsx', info)


@actblueprint.route('/<activity>/change_activity_info')
@get_callsign_decorator(Activity, 'activity')
@special_access_required
def changeactinfo(activity):
    '''Allow club leaders to change activity info'''
    years = (lambda m: map(lambda n: m + n, range(2)))(date.today().year)
    cas = int(activity.cas)
    return render_template('/activity/changeactinfo.html.j2',
                           is_allact=True,
                           years=years,
                           act_types=ActivityTime,
                           cas=cas)


@actblueprint.route('/<activity>/change_activity_info/submit',
                    methods=['POST'])
@get_callsign_decorator(Activity, 'activity')
@special_access_required
def changeactinfo_submit(activity):
    '''Input change in activity info into database'''
    actname = request.form['name']
    if not actname:
        fail('Please input the name of the activity.', 'actinfo')
        return redirect(url_for('.changeactinfo', activity=activity.callsign))
    if actname != activity.name:
        activity.name = actname

    desc = request.form['description'].strip()
    if desc != activity.description.raw:
        activity.description = FormattedText.handle(
            current_user,
            activity.club,
            request.form['description'])
    try:
        actdate = date(int(request.form['year']),
                       int(request.form['month']),
                       int(request.form['day']))
    except ValueError:
        fail('Invalid date.', 'actinfo')
        return redirect(url_for('.changeactinfo', activity=activity.callsign))
    if actdate < date.today():
        fail('Please choose the correct date.', 'actinfo')
        return redirect(url_for('.changeactinfo', activity=activity.callsign))
    activity.date = actdate

    time = ActivityTime[request.form['act_type'].upper()]
    activity.time = time
    activity.location = request.form['location']
    time_type = request.form['time_type']
    try:
        cas = int(request.form['cas'])
    except ValueError:
        fail('Invalid CAS hours.', 'actinfo')
        return redirect(url_for('.changeactinfo', activity=activity.callsign))
    if cas < 0:
        fail('Invalid CAS hours.', 'actinfo')
        return redirect(url_for('.changeactinfo', activity=activity.callsign))
    if time_type == 'hours':
        activity.cas = cas
    else:
        activity.cas = cas / 60

    for member in activity.signup_list():
        member['user'].notify_user('%s\'s information has been changed.'
                                   % activity.name)
    return redirect(url_for('.actintro', activity=activity.callsign))


@actblueprint.route('/<activity>/pairreservation', methods=['GET', 'POST'])
@get_callsign_decorator(Activity, 'activity')
@special_access_required
def pairreservation(activity):
    reservations = Reservation.get_reservations_conditions(

        status=ResStatus.UNPAIRED,