class ProductHuntAPI(object):
    """Wrapper for producthunt api"""

    SCHEMA = "https://"
    HOST = 'api.producthunt.com'
    URL_CLIENT_AUTH = '/v1/oauth/token'
    URL_POSTS = '/v1/posts'
    HEADER = {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Host': 'api.producthunt.com'
    }

    def __init__(self):
        """Create ProductHuntAPI"""
        super(ProductHuntAPI, self).__init__()
        self.access_token = None
        self.expires_in = 0

    def client_auth(self):
        """OAuth Client Only Authentication"""
        current_app.logger.info('start auth on %d' % (int(time.time())))
        data = {
            'client_id': current_app.config["PH_API_KEY"],
            'client_secret': current_app.config["PH_API_SECRET"],
            'grant_type': 'client_credentials'
        }
        url = self.SCHEMA + self.HOST + self.URL_CLIENT_AUTH
        r = requests.post(url, data=json.dumps(data), headers=self.HEADER, verify=False)
        rsp = r.json()
        if r.status_code == 200:
            self.expires_in = int(time.time()) + int(rsp['expires_in']) - 1
            self.access_token = rsp['access_token']
        else:
            current_app.logger.error('auth failed. status_code=%d\n%s'
                % (r.status_code, json.dumps(rsp, sort_keys=True,
                    indent=4, separators=(',', ': '))))
            raise Exception(rsp['error'])

    def posts(self, day=None):
        """Get posts"""
        now = int(time.time())
        if self.access_token is None or self.expires_in < now:
            self.client_auth()

        current_app.logger.info('get posts on %s' % (day or "today"))
        params = None
        if day:
            params = {'day': day}
        url = self.SCHEMA + self.HOST + self.URL_POSTS
        headers = self.HEADER.copy()
        headers['Authorization'] = 'Bearer ' + self.access_token
        r = requests.get(url, headers=headers, params=params, verify=False)
        rsp = r.json()
        if r.status_code == 200:
            return rsp['posts']
        else:
            # access token may expires unexpect, we try it one more time
            if r.status_code == 401:
                self.client_auth()
                r = requests.get(url, headers=headers, verify=False)
                rsp = r.json()
                if r.status_code == 200:
                    return rsp['posts']

            current_app.logger.error('posts failed. status_code=%d\n%s'
                % (r.status_code, json.dumps(rsp, sort_keys=True,
                    indent=4, separators=(',', ': '))))
            raise Exception(rsp['error']) class Product(db.Model):
    __tablename__ = "products"


    # field map between Product and ProductHuntAPI JSON data
    FIELD_MAP = {
        "postid": "id",
        "name": "name",
        "tagline": "tagline",
        "date": "day",
        "redirect_url": "redirect_url",
        "discussion_url": "discussion_url",
        "screenshot_url": "screenshot_url[850px]",
        "votes_count": "votes_count",
        "comments_count": "comments_count",
    }

    id = db.Column(db.Integer, primary_key=True)
    postid = db.Column(db.String(32), unique=True, nullable=False)
    name = db.Column(db.String(128), nullable=False)
    tagline = db.Column(db.Text, nullable=False)
    date = db.Column(db.Date, nullable=False)

    redirect_url = db.Column(db.String(256), nullable=False)
    discussion_url = db.Column(db.String(256), nullable=False)
    screenshot_url = db.Column(db.String(256), nullable=False)

    votes_count = db.Column(db.Integer, default=0)
    comments_count = db.Column(db.Integer, default=0)

    # translate infomation
    # ctagline is a one line translate information
    ctagline = db.Column(db.Text)
    ctagline_locked = db.Column(db.Boolean, default=False)
    ctagline_locked_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    ctagline_locked_user = db.relationship('User',
                                    lazy="joined", uselist=False,
                                    foreign_keys=[ctagline_locked_user_id])
    # cintro is a detail introduct of the product
    cintro = db.Column(db.Text)
    cintro_locked = db.Column(db.Boolean, default=False)
    cintro_locked_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    cintro_locked_user = db.relationship('User',
                                    lazy="joined", uselist=False,
                                    foreign_keys=[cintro_locked_user_id])
    # user who translate this product
    editing_ctagline_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    editing_ctagline_user = db.relationship('User',
                                    lazy="joined",
                                    backref="product_translating", uselist=False,
                                    foreign_keys=[editing_ctagline_user_id])

    # user who write introduce article to this product
    editing_cintro_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    editing_cintro_user = db.relationship('User',
                                    lazy="joined",
                                    backref="product_introducing", uselist=False,
                                    foreign_keys=[editing_cintro_user_id])

    tags = db.relationship('Tag',
                    secondary=products_tags,
                    primaryjoin=(products_tags.c.product_postid == postid),
                    backref=db.backref('products', lazy='dynamic'),
                    lazy='dynamic')

    # Methods
    def __repr__(self):
        """Set to a unique key specific to the object in the database.
        Required for cache.memoize() to work across requests.
        """
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def save(self):
        """Saves a product"""
        if not self.cintro:
            self.cintro = _MD_TEMPLATE % (self.name, self.redirect_url, \
                self.tagline, self.screenshot_url)
        db.session.add(self)
        db.session.commit()
        return self

    def add_tag(self, tag):
        """Adds the product to the `tag` if he isn't in it.

        :param tag: The tag which should be added to the product.
        """

        if not self.has_tag(tag):
            self.tags.append(tag)
            return self

    def remove_tag(self, tag):
        """Removes the product from the `tag` if it is in it.

        :param tag: The tag which should be removed from the product.
        """

        if self.has_tag(tag):
            self.tags.remove(tag)
            return self

    def has_tag(self, tag):
        """Returns True if the product has the specified tag

        :param tag: The tag which should be checked.
        """

        return self.tags.filter(products_tags.c.tag_id == tag.id).count() > 0

    def set_tags(self, tagnames):
        """set post tags"""

        # remove old tags
        for tag in self.tags:
            self.remove_tag(tag)

        tagnames = tagnames.split(';')
        for name in tagnames:
            if name.strip():
                tag = Tag.from_name(name)
                self.add_tag(tag)
        db.session.commit()

    @classmethod
    def from_json(cls, json):
        """
        Create from json data or update the exist one
        """
        p = cls.query.filter(cls.postid==json['id']).first()
        if not p:
            p = cls()
        for k, v in cls.FIELD_MAP.items():
            if '[' in v:
                m = re.search(r"^(.+)\[(.+)\]$", v)
                setattr(p, k, json[m.group(1)][m.group(2)])
            elif k == 'date':
                ymd = json[v].split('-')
                p.date = datetime.date(int(ymd[0]), int(ymd[1]), int(ymd[2]))
            else:
                setattr(p, k, json[v])
        return p class Tag(db.Model):
    __tablename__ = "tags"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), nullable=False)
    description = db.Column(db.Text)

    # Methods
    def __repr__(self):
        """Set to a unique key specific to the object in the database.
        Required for cache.memoize() to work across requests.
        """
        return "<{} {}>".format(self.__class__.__name__, self.id)

    def save(self):
        """Saves a tag"""
        db.session.add(self)
        db.session.commit()
        Tag.invalidate_cache()
        return self

    def delete(self):
        """Deletes a tag"""
        db.session.delete(self)
        db.session.commit()
        Tag.invalidate_cache()
        return self

    @classmethod
    @cache.memoize(timeout=max_integer)
    def tag_names(cls, limit):
        """return the all tags name array"""

        tags = Tag.query.order_by(Tag.id.asc()).limit(limit).offset(0).all()
        tagnames = []
        for tag in tags:
            tagnames.append(tag.name)
        return tagnames

    @classmethod
    def names(cls, limit=20):
        """return tag name array"""

        return cls.tag_names(limit)

    @classmethod
    def invalidate_cache(cls):
        """Invalidates this objects cached metadata."""

        cache.delete_memoized(cls.tag_names)

    @classmethod
    def from_name(cls, name):
        """
        Create from tag from name or return the tag with the specific name
        """
        name = name.strip()
        tag = cls.query.filter(cls.name==name).first()
        if not tag:
            tag = cls(name=name)
            tag.save()
        return tag def render_template(template, **context):
    """
    A helper function that uses the `render_theme_template` function
    without needing to edit all the views
    """
    theme = current_app.config['DEFAULT_THEME']
    return render_theme_template(theme, template, **context) def pull_and_save_posts(day=None):
    """
    Pull and save posts to database
    """
    api = ProductHuntAPI()
    posts = api.posts(day)
    for jsondata in posts:
        pi = Product.from_json(jsondata)
        pi.save()
    return len(posts) def render_markup(text):
    """Renders the given text as markdown

    :param text: The text to be rendered
    """
    if text is None:
        text = ""
    return render_markdown(text, extras=['tables']) def query_products(spec_day=None):
    """ get all the products of the day """
    day = spec_day
    if not day:
        day = format_date(datetime.date.today())
    posts = Product.query.filter(Product.date==day).\
        order_by(Product.votes_count.desc()).all()

    # when not specific a day and the content is empty, we show yesterday's data
    if not spec_day and len(posts) == 0:
        delta = datetime.timedelta(days=-1)
        d = datetime.date.today() + delta
        day = format_date(d)
    posts = Product.query.filter(Product.date==day).\
        order_by(Product.votes_count.desc()).all()

    # when spec_day is some old date and data is empty, we pull from PH server
    if spec_day and len(posts) == 0:
        today = datetime.date.today()
        ymd = spec_day.split('-')
        spec_date = datetime.date(int(ymd[0]), int(ymd[1]), int(ymd[2]))
        if spec_date < today:
            day = spec_day
            pull_and_save_posts(day)
            posts = Product.query.filter(Product.date==day).\
                order_by(Product.votes_count.desc()).all()
    return day, posts def can_translate(user):
    """Checks if a user translate a product"""

    return user.permissions['perm_translate'] def can_review(user):
    """Checks if a user can review a translate"""

    return user.permissions['perm_review'] def is_online(user):
    """A simple check to see if the user was online within a specified
    time range

    :param user: The user who needs to be checked
    """
    return user.lastseen >= time_diff() def moderator_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if current_user.is_anonymous():
            abort(403)

        if not any([current_user.permissions['admin'],
                    current_user.permissions['mod']]):
            abort(403)

        return f(*args, **kwargs)
    return decorated class User(db.Model, UserMixin):
    __tablename__ = "users"
    __searchable__ = ['username', 'email']

    # field map between User and Weibo API users/show json data
    FIELD_MAP = {
        "wbuid": "id",
        "username": "screen_name",
        "gender": "gender",
        "website": "url",
        "notes": "description",
        "nickname": "name",
        "avatar": "avatar_large",
    }

    id = db.Column(db.Integer, primary_key=True)
    wbuid = db.Column(db.String(16))
    username = db.Column(db.String(200), unique=True, nullable=False)
    email = db.Column(db.String(200), unique=True, nullable=False)
    _password = db.Column('password', db.String(120), nullable=False)
    date_joined = db.Column(db.DateTime, default=datetime.utcnow())
    lastseen = db.Column(db.DateTime, default=datetime.utcnow())
    birthday = db.Column(db.DateTime)
    gender = db.Column(db.String(10))
    website = db.Column(db.String(512))
    location = db.Column(db.String(100))
    nickname = db.Column(db.String(100))
    avatar = db.Column(db.String(512))
    notes = db.Column(db.Text)

    theme = db.Column(db.String(15))

    primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'),
                                 nullable=False)

    primary_group = db.relationship('Group', lazy="joined",
                                    backref="user_group", uselist=False,
                                    foreign_keys=[primary_group_id])

    secondary_groups = \
        db.relationship('Group',
                        secondary=groups_users,
                        primaryjoin=(groups_users.c.user_id == id),
                        backref=db.backref('users', lazy='dynamic'),
                        lazy='dynamic')

    ctagline_products = \
        db.relationship('Product',
                        secondary=users_translated,
                        primaryjoin=(users_translated.c.user_id == id),
                        backref=db.backref('ctagline_editors', lazy='dynamic'),
                        lazy='dynamic')

    cintro_products = \
        db.relationship('Product',
                        secondary=users_introduced,
                        primaryjoin=(users_introduced.c.user_id == id),
                        backref=db.backref('cintro_editors', lazy='dynamic'),
                        lazy='dynamic')

    @property
    def url(self):
        """Returns the url for the user"""
        return url_for("user.profile", username=self.username)

    @property
    def permissions(self):
        """Returns the permissions for the user"""
        return self.get_permissions()

    @property
    def days_registered(self):
        """Returns the amount of days the user is registered."""
        days_registered = (datetime.utcnow() - self.date_joined).days
        if not days_registered:
            return 1
        return days_registered

    # Methods
    def __repr__(self):
        """Set to a unique key specific to the object in the database.
        Required for cache.memoize() to work across requests.
        """
        return "<{} {}>".format(self.__class__.__name__, self.username)

    def _get_password(self):
        """Returns the hashed password"""
        return self._password

    def _set_password(self, password):
        """Generates a password hash for the provided password"""
        self._password = generate_password_hash(password)

    # Hide password encryption by exposing password field only.
    password = db.synonym('_password',
                          descriptor=property(_get_password,
                                              _set_password))

    def check_password(self, password):
        """Check passwords. If passwords match it returns true, else false"""

        if self.password is None:
            return False
        return check_password_hash(self.password, password)

    @classmethod
    def authenticate(cls, login, password):
        """A classmethod for authenticating users
        It returns true if the user exists and has entered a correct password

        :param login: This can be either a username or a email address.
        :param password: The password that is connected to username and email.
        """

        user = cls.query.filter(db.or_(User.username == login,
                                       User.email == login)).first()

        if user:
            authenticated = user.check_password(password)
        else:
            authenticated = False
        return user, authenticated

    def _make_token(self, data, timeout):
        s = Serializer(current_app.config['SECRET_KEY'], timeout)
        return s.dumps(data)

    def _verify_token(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        data = None
        expired, invalid = False, False
        try:
            data = s.loads(token)
        except SignatureExpired:
            expired = True
        except Exception:
            invalid = True
        return expired, invalid, data

    def make_reset_token(self, expiration=3600):
        """Creates a reset token. The duration can be configured through the
        expiration parameter.

        :param expiration: The time in seconds how long the token is valid.
        """
        return self._make_token({'id': self.id, 'op': 'reset'}, expiration)

    def verify_reset_token(self, token):
        """Verifies a reset token. It returns three boolean values based on
        the state of the token (expired, invalid, data)

        :param token: The reset token that should be checked.
        """

        expired, invalid, data = self._verify_token(token)
        if data and data.get('id') == self.id and data.get('op') == 'reset':
            data = True
        else:
            data = False
        return expired, invalid, data

    def add_to_group(self, group):
        """Adds the user to the `group` if he isn't in it.

        :param group: The group which should be added to the user.
        """

        if not self.in_group(group):
            self.secondary_groups.append(group)
            db.session.commit()
            self.invalidate_cache()
            return self

    def remove_from_group(self, group):
        """Removes the user from the `group` if he is in it.

        :param group: The group which should be removed from the user.
        """

        if self.in_group(group):
            self.secondary_groups.remove(group)
            db.session.commit()
            self.invalidate_cache()
            return self

    def in_group(self, group):
        """Returns True if the user is in the specified group

        :param group: The group which should be checked.
        """

        return self.secondary_groups.filter(
            groups_users.c.group_id == group.id).count() > 0

    @cache.memoize(timeout=max_integer)
    def get_permissions(self, exclude=None):
        """Returns a dictionary with all the permissions the user has.

        :param exclude: a list with excluded permissions. default is None.
        """

        exclude = exclude or []
        exclude.extend(['id', 'name', 'description'])

        perms = {}
        groups = self.secondary_groups.all()
        groups.append(self.primary_group)
        for group in groups:
            for c in group.__table__.columns:
                # try if the permission already exists in the dictionary
                # and if the permission is true, set it to True
                try:
                    if not perms[c.name] and getattr(group, c.name):
                        perms[c.name] = True

                # if the permission doesn't exist in the dictionary
                # add it to the dictionary
                except KeyError:
                    # if the permission is in the exclude list,
                    # skip to the next permission
                    if c.name in exclude:
                        continue
                    perms[c.name] = getattr(group, c.name)
        return perms

    def invalidate_cache(self):
        """Invalidates this objects cached metadata."""

        cache.delete_memoized(self.get_permissions, self)

    def in_ctagline_list(self, product):
        """check does product in users translated list"""

        return self.ctagline_products.filter(
            users_translated.c.product_postid == product.postid).count() > 0

    def add_ctagline_product(self, product):
        """add a product to users translate list"""
        if not self.in_ctagline_list(product):
            self.ctagline_products.append(product)
            db.session.commit()
            return self

    def in_cintro_list(self, product):
        """check does product in users translated list"""

        return self.cintro_products.filter(
            users_introduced.c.product_postid == product.postid).count() > 0

    def add_cintro_product(self, product):
        """add a product to users translate list"""
        if not self.in_cintro_list(product):
            self.cintro_products.append(product)
            db.session.commit()
            return self

    @classmethod
    def from_json(cls, json):
        """
        Create User from weibo json data
        """
        u = User()
        for k, v in User.FIELD_MAP.items():
            if k == 'gender':
                if json[v] == 'm':
                    u.gender = 'Male'
                elif json[v] == 'f':
                    u.gender = 'Female'
            else:
                setattr(u, k, json[v])

        # special field
        u.email = u.username + '@example.org'
        u.password = ''.join(random.sample( \
            '1234567890zyxwvutsrqponmlkjihgfedcba',8))
        member_group = Group.query.filter_by(member=True).first()
        u.primary_group_id = member_group.id
        return u

    def save(self):
        """Saves a user"""

        db.session.add(self)
        db.session.commit()
        return self

    def delete(self):
        """Deletes the User."""

        db.session.delete(self)
        db.session.commit()

        return self
import datetime
import json
from flask import Blueprint, request, current_app, flash, redirect, \
    url_for, jsonify, make_response
from flask.ext.login import current_user
from qiniu import Auth
from productporter.product.phapi import ProductHuntAPI
from productporter.product.models import Product, Tag
from productporter.utils.helper import render_template, pull_and_save_posts, render_markup, \
    query_products, can_translate, can_review, is_online
from productporter.utils.decorators import moderator_required
from productporter.user.models import User
                }
            return jsonify(**ret)
    except KeyError:
        pass

    current_app.logger.info('commit %s for post %s' % (field, str(postid)))

    # deal with tags
    if field == 'ctagline':
        post.set_tags(jsondata['tags'])
    # deal with other filed data
    setattr(post, field, jsondata['value'])
    setattr(post, 'editing_' + field + '_user_id', None)
    post.save()
    getattr(current_user, 'add_' + field + '_product')(post)
    ret = {
        'status': 'success',
        'postid': post.postid,
        'field': field,
        'value': render_markup(getattr(post, field)),
        'contributors': _render_contributors( \
            getattr(post, field + '_editors'), post.postid, \
            getattr(post, field + '_locked_user'), field),
        'tags': _render_tags(post)
    }

    return jsonify(**ret)

# posts list
@product.route('/', methods=["GET"])
def index():
    """ product posts home dashboard """
    return redirect(url_for('product.posts'))

# posts list
@product.route('/posts/', methods=["GET"])
def posts():
    """ product posts home dashboard """
    spec_day = request.args.get('day', '')
    day, posts = query_products(spec_day)
    post_count = len(posts)
    tags = Tag.names()
    return render_template('product/posts.jinja.html',
        post_count=post_count, posts=posts, day=day, tags=tags)

# posts list
@product.route('/posts/<postid>', methods=["GET"])
def post_intro(postid):
    """ product detail information page """

    post = Product.query.filter(Product.postid==postid).first_or_404()
    tags = Tag.names()
    return render_template('product/post_intro.jinja.html', post=post, tags=tags)

#pull products
@product.route('/pull')
def pull():
    """ pull data from producthunt.com """
    day = request.args.get('day', '')
    count = pull_and_save_posts(day)
    return "pulled %d posts " % (count)

@product.route('/lock', methods=['GET'])
@moderator_required
def lock():
    """
    lock product

    :param postid: The postid of product
    :param op: Operation, clould be 'lock' or 'unlock'
    :param field: Field, could be 'ctagline' or 'cintro'
    """
    postid = request.args.get('postid', '')
    op = request.args.get('op', 'lock')
    field = request.args.get('field', 'ctagline')
    post = Product.query.filter(Product.postid==postid).first_or_404()

    if op.lower() == 'lock':
        setattr(post, field + '_locked', True)
        setattr(post, field + '_locked_user_id', current_user.id)
        op = 'Unlock'
    else:
        setattr(post, field + '_locked', False)
        setattr(post, field + '_locked_user_id', None)
        op = 'Lock'
    post.save()
    ret = {
        'status': 'success',
        'postid': post.postid,
        'contributors': _render_contributors( \
            getattr(post, field + '_editors'), post.postid, \
            getattr(post, field + '_locked_user'), field)
    }
    return jsonify(**ret)

@product.route('/tags/', methods=["GET"])
def tags():
    """show all products"""
    return "under construction"

@product.route('/tags/<tagname>', methods=["GET"])
def tags_name(tagname):
    """show all products by selected tag"""
    return "under construction"

@product.route('/dailybriefing/<day>', methods=['GET'])
@moderator_required
def dailybriefing(day):
    """ Generate daily briefing """
    qday, posts = query_products(day)
    post_count = len(posts)

    # Thanks to contributors
    editors = []
    for post in posts:
        if post.ctagline and post.ctagline_locked:
            editors += post.ctagline_editors
    # Thank once is enough
    editors = {}.fromkeys(editors).keys()


    return render_template('product/dailybriefing.jinja.html',