class Club(BaseObject):
    table = 'club'
    identifier = 'club_id'
    name = Property('club_name', search=True)
    teacher = Property('club_teacher', 'User')
    leader = Property('club_leader', 'User')
    description = Property('club_desc', 'FormattedText', search=True)
    location = Property('club_location')
    is_active = Property('club_inactive', lambda v: not v,
                         search_require_true=True)
    intro = Property('club_intro', search=True)
    picture = Property('club_picture', 'Upload')
    type = Property('club_type', ClubType)
    joinmode = Property('club_joinmode', ClubJoinMode)
    reactivate = Property('club_reactivate', bool)
    reservation_allowed = Property('club_reservation_allowed', bool)
    smartboard_allowed = Property('club_smartboard_allowed', bool)
    smartboard_teacherapp_bypass = \
        Property('club_smartboard_teacherapp_bypass', bool)
    smartboard_directorapp_bypass = \
        Property('club_smartboard_directorapp_bypass', bool)
    members = ListProperty('club_member', 'cm_club', 'cm_user', 'User')
    all_act = ListProperty('activities', 'act_club', 'act_id', 'Activity')

    @property
    def teacher_and_members(self):
        ret = set(self.members)
        ret.add(self.teacher)
        return ret

    @property
    def is_excellent(self):
        return self in self.excellentclubs()

    @staticmethod
    def _excellentclubs():
        return redis.RedisList('excellentclubs', -1)

    # We can't use @property because fails with Club.excellentclubs = []
    # @deprecated
    @classmethod
    def excellentclubs(cls, amount=None):
        ret = cls.allclubs(excellent_only=True,
                           random_order=bool(amount), limit=amount)
        if amount:
            return ret[1]
        else:
            return ret

    @classmethod
    def set_excellentclubs(cls, newval):
        newval = [item.id for item in newval]
        lst = cls._excellentclubs()
        lst[:] = newval

    # @deprecated
    @classmethod
    def randomclubs(cls, amount, *args, **kwargs):
        ret = cls.allclubs(random_order=True, limit=amount,
                           *args, **kwargs)
        try:
            return ret[1]
        except IndexError:
            return ret

    @classmethod
    @paged_db_read
    def allclubs(cls, club_types=None, excellent_only=False, grade_limit=(),
                 active_only=True, additional_conds=(), random_order=False,
                 pager=None):
        conds = {}
        if additional_conds:
            conds.update(additional_conds)

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

        if club_types:
            types = [club_type.value for club_type in club_types]
            conds['where'].append(('in', 'club_type', types))
        if excellent_only:
            conds['where'].append(('in', 'club_id', cls._excellentclubs()))
        if grade_limit:
            conds['join'] = conds.get('join', [])
            conds['join'].append(
                ('inner', 'user', [('user_id', 'club_leader')]))
            conds['where'].append(('in', 'user_grade', grade_limit))
        if active_only:
            conds['where'].append(('=', 'club_inactive', False))
            conds['where'].append(('=', 'club_reactivate', True))
        if random_order:
            conds['order'] = conds.get('order', [])
            conds['order'].append((database.RawSQL('RAND()'), True))

        pager_fetch, pager_return = pager

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

    def activities(self, times=(), dates=(True, True), **kwargs):
        from oclubs.objs import Activity

        return Activity.get_activities_conditions(
            times,
            {
                'where': [('=', 'act_club', self.id)],
            },
            dates=dates,
            order_by_time=True,
            **kwargs
        )

    @paged_db_read
    def allactphotos(self, pager=None):
        from oclubs.objs import Activity, Upload

        pager_fetch, pager_return = pager
        tempdata = pager_fetch(
            database.fetch_multirow,
            'act_pic',
            {
                'actpic_upload': 'upload',
                'act_id': 'activity',
            },
            {
                'join': [('inner', 'activity', [('actpic_act', 'act_id')])],
                'where': [('=', 'act_club', self.id)],
            }
        )

        for item in tempdata:
            item['upload'] = Upload(item['upload'])
            item['activity'] = Activity(item['activity'])

        return pager_return(tempdata)

    def add_member(self, user):
        database.insert_row('club_member',
                            {'cm_club': self.id, 'cm_user': user.id})
        user.delete_invitation(self)
        del self.members

    def remove_member(self, user):
        database.delete_rows('club_member',
                             {'cm_club': self.id, 'cm_user': user.id})
        del self.members

    def send_invitation(self, user):
        from oclubs.exceptions import AlreadyExists

        try:
            database.insert_row(
                'invitation',
                {'invitation_club': self.id,
                 'invitation_user': user.id,
                 'invitation_date': dateobj_to_int(today())}
            )
        except AlreadyExists:
            pass

    @classmethod
    def get_clubs_special_access(cls, user):
        if user.type == UserType.STUDENT:
            ret = database.fetch_onecol(
                cls.table,
                cls.identifier,
                {'club_leader': user.id},
            )
        elif user.type == UserType.TEACHER:
            ret = database.fetch_onecol(
                cls.table,
                cls.identifier,
                {'club_teacher': user.id},
            )
        else:  # ADMIN
            ret = []

        return map(cls, ret) 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 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] 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 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 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) 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 fail(msg, group):
    flash(msg, group)
    g.hasfailures = True def read_xlsx(file, data_type, header):
    '''Read xlsx and return a list of data'''
    raw = get_data(file)
    data = raw[data_type]
    if [d.lower().strip().replace(' ', '') for d in data[0]] != header:
        raise ValueError
    contents = data[1:]
    return contents class PasswordTooShort(Exception):
    """Exception occuring someone sets a password that's too short."""
    pass FILENAME = '/srv/oclubs/siteconfig.ini'
def _done(commit=True):
def _get_parser():
def get_config(name):
def set_config(name, value): def send(to_email, subject, content):
from collections import defaultdict
from datetime import date
from flask import (
    Blueprint, render_template, url_for, request, redirect, abort, flash,
    jsonify
)
from flask_login import current_user, login_required, fresh_login_required
from oclubs.objs import User, Club, Upload
from oclubs.enums import UserType, ActivityTime
from oclubs.shared import (
    special_access_required, download_xlsx, render_email_template, Pagination,
    fail, read_xlsx
)
from oclubs.exceptions import PasswordTooShort
from oclubs.access import siteconfig, email
    from oclubs.worker import refresh_user
    from oclubs.worker import rebuild_elasticsearch
    if 'picture' in request.form:
        pic = int(request.form['picture'])
        if -pic in range(1, 21):
            current_user.picture = Upload(pic)
    if 'receive_email' in request.form:
        current_user.set_preference('receive_email', True)
    else:
        current_user.set_preference('receive_email', False)
    flash('Your information has been successfully changed.', 'status_info')
    return redirect(url_for('.personal'))


@userblueprint.route('/submit_password', methods=['POST'])
@login_required
def personalsubmitpassword():
    '''Change user's password in database'''
    user_login = User.attempt_login(current_user.passportname,
                                    request.form['old'])
    if user_login is None:
        fail('You have entered wrong old password. Please enter again.',
             'status_pw')
    elif request.form['new'] == '':
        fail('Please enter new password.', 'status_pw')
    elif request.form['new'] != request.form['again']:
        fail('You have entered two different passwords. '
             'Please enter again.', 'status_pw')
    else:
        try:
            current_user.password = request.form['new']
            flash('Your password has been successfully changed.',
                  'status_pw')
        except PasswordTooShort:
            fail('Password must be at least six digits.', 'status_pw')
    return redirect(url_for('.personal'))


@userblueprint.route('/info_download_all')
@special_access_required
@fresh_login_required
def allusersinfo():
    '''Allow admin to download all users' info'''
    info = []
    info.append(('Student ID', 'Class', 'Nick Name', 'Passport Name/Username', 'Email',
                 'Phone'))
    info.extend([(user.studentid, user.grade_and_class, user.nickname,
                  user.passportname, user.email, str(user.phone))
                 for user in User.allusers()])

    return download_xlsx('All Users\' Info.xlsx', info)


@userblueprint.route('/refresh_users')
@special_access_required
@fresh_login_required
def refreshusers():
    return render_template('user/refreshuser.html.j2')


@userblueprint.route('/refresh_users/submit', methods=['POST'])
@special_access_required
@fresh_login_required
def refreshusers_submit_old():
    '''Upload excel file to create new users'''
    if request.files['excel'].filename == '':
        fail('Please upload an excel file.', 'refresh_users')
        return redirect(url_for('.refreshusers'))
    try:
        contents = read_xlsx(request.files['excel'], 'Students',
                             ['studentid', 'passportname', 'class'])
    except KeyError:
        fail('Please change sheet name to "Students"', 'refresh_users')
        return redirect(url_for('.refreshusers'))
    except ValueError:
        fail('Please input in the correct order.', 'refresh_users')
        return redirect(url_for('.refreshusers'))

    authority = {}
    for student in contents:
        studentid, passportname, gradeclass = student
        studentid = str(studentid)
        grade, curclass = User.extract_gradeclass(gradeclass)
        if studentid in authority:
            fail('Duplicate Student ID "%s"' % studentid, 'refresh_users')
            return redirect(url_for('.refreshusers'))
        authority[studentid] = {
            'UNIONID': studentid,
            'NAMEEN': passportname,
            'GRADENAME': grade,
            'STUCLASSNAME': curclass,
        }

    refresh_user.delay(authority)

    flash('Student accounts\' information has been successfully '
          'scheduled to refresh.', 'refresh_users')
    return redirect(url_for('.refreshusers'))


@userblueprint.route('/rebuild_elastic_search/submit', methods=['POST'])
@special_access_required
@fresh_login_required
def rebuildsearch_submit():
    '''Rebuild elastic search engine to fix asyncronized situation'''
    rebuild_elasticsearch.delay()
    flash('Search engine has been scheduled to fix.', 'rebuild_search')
    return redirect(url_for('.personal'))


@userblueprint.route('/download_new_passwords')
@special_access_required
@fresh_login_required
def download_new_passwords():
    '''Allow admin to download new accounts' passwords'''
    result = []
    result.append(['Student ID', 'Passport Name/Username', 'Class', 'Password'])
    users = User.get_new_passwords()
    result.extend([(user.studentid,
                    user.passportname,
                    user.grade_and_class,
                    password) for user, password in users])

    return download_xlsx('New Accounts\' Passwords.xlsx', result)