Exemple #1
0
 def tag_assoc(cls):
     return db.Table(
         '{}_tag'.format(cls.__tablename__),
         db.Column('{}_id'.format(cls.__tablename__),
                   db.ForeignKey('{}.id'.format(cls.__tablename__)),
                   primary_key=True),
         db.Column('tag_id', db.ForeignKey(Tag.id), primary_key=True))
Exemple #2
0
class Transcript(IDModel):
    title = db.Column(db.String, nullable=False)
    ts = db.Column(db.DateTime, nullable=False)
    body = db.Column(db.String, nullable=False, default='')

    messages = db.relationship(ChatMessage,
                               lambda: transcript_message,
                               order_by=ChatMessage.id,
                               cascade='all')

    def __str__(self):
        return self.title

    @property
    def detail_url(self):
        return url_for('transcript.detail', id=self)

    @property
    def update_url(self):
        return url_for('transcript.update', id=self.id)

    @property
    def delete_url(self):
        return url_for('transcript.delete', id=self.id)

    @property
    def local_time_url(self):
        query = quote_plus('{} utc in local time'.format(
            self.ts.strftime('%Y-%m-%d %H:%M')))
        return 'http://www.wolframalpha.com/input?i={}'.format(query)
class SEUser(ExternalIDModel):
    display_name = db.Column(db.String, nullable=False)
    profile_image = db.Column(db.String, nullable=False)
    profile_link = db.Column(db.String, nullable=False)
    reputation = db.Column(db.Integer, nullable=False)

    def __str__(self):
        return self.display_name

    @classmethod
    def se_load(cls, ident, update=True):
        """Load a user by id.  If the user is not in the database, retrieve them with the API.

        If the same user may be (re-)cached multiple times in one operation, you can pass `update=False` to only retrieve the data from the API if the user hasn't been cached.

        :param ident: user id to load
        :param update: if False, an API call will only be made if this user hasn't been cached
        :return: user instance
        """

        try:
            id = int(ident)
        except ValueError:
            match = user_id_re.search(ident)
            id = int(match.group(1))

        o = cls.get_unique(id=id)

        # only update if told to or if the instance hasn't been cached
        if update or o.display_name is None:
            o.se_update()

        return o

    def se_update(self, data=None):
        if data is None:
            r = requests.get(users_url.format(self.id), params={
                'key': current_app.config.get('SE_API_KEY'),
                'site': 'stackoverflow',
            })
            items = r.json()['items']
            data = items[0] if items else False

        if data is False and self.display_name is None:
            # this user hasn't been cached, but can't be retrieved from the API
            # so create a dummy user
            self.display_name = 'user{}'.format(self.id)
            self.profile_image = ''
            self.profile_link = ''
            self.reputation = -1
        elif data:
            self.display_name = data['display_name']
            self.profile_image = data['profile_image']
            self.profile_link = data['link']
            self.reputation = data['reputation']

        return self
class SEQuestion(HasTags, ExternalIDModel):
    title = db.Column(db.String, nullable=False)
    body = db.Column(db.String, nullable=False)
    link = db.Column(db.String, nullable=False)

    @classmethod
    def se_load(cls, ident):
        """Load SO data given a question id or link.

        If the question exists in the local db, it will be updated, otherwise it will be created.

        :param ident: question id or link
        :return: instance populated loaded data
        """

        try:
            id = int(ident)
        except ValueError:
            match = question_id_re.search(ident)
            id = int(match.group(1))

        o = cls.get_unique(id=id)
        r = requests.get(questions_url.format(id), params={
            'key': current_app.config.get('SE_API_KEY'),
            'site': 'stackoverflow',
            'filter': '!5RCKN561Hrx5Mj7Pc*qRTOUCj',
        })
        data = r.json()['items'][0]

        #TODO: error checking

        return o.se_update(data)

    def se_update(self, data=None):
        """Update question based on latest SO data.

        :param data: pre-requested data, or None to load the data now
        :return: updated instance
        """

        if data is None:
            r = requests.get(questions_url.format(self.id), params={
                'key': current_app.config.get('SE_API_KEY'),
                'site': 'stackoverflow',
                'filter': '!5RCKN561Hrx5Mj7Pc*qRTOUCj',
            })
            data = r.json()['items'][0]

        self.title = data['title']
        self.body = data['body_markdown']
        self.link = data['link']
        self.tags.update(data['tags'])

        return self
Exemple #5
0
class User(UserMixin, SEUser):
    __tablename__ = 'user'

    id = db.Column(db.Integer, db.ForeignKey(SEUser.id), primary_key=True)
    superuser = db.Column(db.Boolean, nullable=False, default=False)

    _groups = db.relationship(Group,
                              lambda: user_group,
                              collection_class=set,
                              backref=db.backref('users',
                                                 collection_class=set))
    groups = association_proxy('_groups', 'name', creator=Group.get_unique)

    authenticated = True
    anonymous = False

    def has_group(self, *groups):
        if self.superuser:
            return True

        for group in groups:
            if isinstance(group, str):
                if group in self.groups:
                    return True

            if group in self._groups:
                return True

        return any(sub.has_group(*groups) for sub in self._groups)

    @classmethod
    def oauth_load(cls, token=None):
        r = requests.get('https://api.stackexchange.com/2.2/me',
                         params={
                             'key': current_app.config['SE_API_KEY'],
                             'access_token': token or session['oauth_token'],
                             'site': 'stackoverflow',
                         })
        data = r.json()['items'][0]

        o = cls.get_unique(id=data['user_id'])

        return o.se_update(data)

    @classmethod
    def create_unique(cls, session, id):
        # if the site user was cached, manually upgrade it to a local user
        if SEUser.query.get(id) is not None:
            session.execute(cls.__table__.insert().values(id=id))
            return cls.query.get(id)

        o = cls(id=id)
        session.add(o)
        return o
Exemple #6
0
class WikiPage(IDModel):
    title = db.Column('title', db.String, nullable=False, unique=True)
    body = db.Column(db.String, nullable=False)
    updated = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
    draft = db.Column(db.Boolean, nullable=False, default=False)
    community = db.Column(db.Boolean, nullable=False, default=False)
    author_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
    redirect_id = db.Column(db.Integer, db.ForeignKey('wiki_page.id'))

    author = db.relationship(User)
    redirect = db.relationship(lambda: WikiPage, remote_side=lambda: (WikiPage.id,), backref='redirects')

    def __str__(self):
        return self.title

    @property
    def detail_url(self):
        return url_for('wiki.detail', title=self.title)

    @property
    def update_url(self):
        return url_for('wiki.update', title=self.title)

    @property
    def delete_url(self):
        return url_for('wiki.delete', title=self.title)
Exemple #7
0
class IDModel(db.Model):
    __abstract__ = True

    id = db.Column(db.Integer, primary_key=True)

    def __str__(self):
        return str(self.id)

    def compare_value(self):
        return self.id
Exemple #8
0
class CanonItem(HasTags, IDModel):
    title = db.Column(db.String, nullable=False)
    excerpt = db.Column(db.String, nullable=False, default='')
    body = db.Column(db.String, nullable=False, default='')
    draft = db.Column(db.Boolean, nullable=False, default=True)
    community = db.Column(db.Boolean, nullable=False, default=False)
    updated_by_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)

    updated_by = db.relationship(User)

    def __str__(self):
        return self.title

    questions = db.relationship(SEQuestion, lambda: canon_item_se_question, collection_class=set)
    question_links = association_proxy('questions', 'link', creator=SEQuestion.se_load)

    @property
    def detail_url(self):
        return url_for('canon.detail', id=self)

    @property
    def update_url(self):
        return url_for('canon.update', id=self.id)

    @property
    def delete_url(self):
        return url_for('canon.delete', id=self.id)
Exemple #9
0
class Tag(IDModel):
    name = db.Column(db.String, nullable=False, unique=True)

    def __init__(self, name=None, **kwargs):
        if name is not None:
            kwargs['name'] = name

        super(Tag, self).__init__(**kwargs)

    def __str__(self):
        return self.name

    def compare_value(self):
        return self.name

    @classmethod
    def get_unique(cls, name, **kwargs):
        return super(Tag, cls).get_unique(name=name, **kwargs)
Exemple #10
0
class Group(IDModel):
    name = db.Column(db.String, nullable=False, unique=True)

    _groups = db.relationship(
        lambda: Group,
        lambda: group_group,
        primaryjoin=lambda: Group.id == group_group.c.member_id,
        secondaryjoin=lambda: Group.id == group_group.c.group_id,
        collection_class=set,
        backref=db.backref('_members', collection_class=set))
    groups = association_proxy('_groups',
                               'name',
                               creator=lambda x: Group.get_unique(x))
    members = association_proxy('_members',
                                'name',
                                creator=lambda x: Group.get_unique(x))

    def __init__(self, name=None, **kwargs):
        if name is not None:
            kwargs['name'] = name

        super(Group, self).__init__(**kwargs)

    def __str__(self):
        return self.name

    def compare_value(self):
        return self.name

    @classmethod
    def get_unique(cls, name, **kwargs):
        return super(Group, cls).get_unique(name=name, **kwargs)

    def has_group(self, *groups):
        for group in groups:
            if isinstance(group, str):
                if group in self.groups:
                    return True

            if group in self._groups:
                return True

        return any(sub.has_group(*groups) for sub in self._groups)
Exemple #11
0
class ExternalIDModel(IDModel):
    __abstract__ = True

    id = db.Column(db.Integer, primary_key=True, autoincrement=False)
Exemple #12
0
    draft = db.Column(db.Boolean, nullable=False, default=True)
    community = db.Column(db.Boolean, nullable=False, default=False)
    updated_by_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)

    updated_by = db.relationship(User)

    def __str__(self):
        return self.title

    questions = db.relationship(SEQuestion, lambda: canon_item_se_question, collection_class=set)
    question_links = association_proxy('questions', 'link', creator=SEQuestion.se_load)

    @property
    def detail_url(self):
        return url_for('canon.detail', id=self)

    @property
    def update_url(self):
        return url_for('canon.update', id=self.id)

    @property
    def delete_url(self):
        return url_for('canon.delete', id=self.id)


canon_item_se_question = db.Table(
    'canon_item_se_question',
    db.Column('canon_item_id', db.Integer, db.ForeignKey(CanonItem.id), primary_key=True),
    db.Column('se_question_id', db.Integer, db.ForeignKey(SEQuestion.id), primary_key=True)
)
Exemple #13
0
class ChatMessage(ExternalIDModel):
    room_id = db.Column(db.Integer, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey(SEUser.id), nullable=False)
    ts = db.Column(db.DateTime, nullable=False)
    content = db.Column(db.String, nullable=False)
    rendered = db.Column(db.Boolean, nullable=False)
    stars = db.Column(db.Integer, nullable=False, default=0)

    user = db.relationship(SEUser, backref='chat_messages')

    # this almost certainly does not belong here
    @classmethod
    def get_active_users(cls):
        """ returns the active users in the chat room currently """
        room_url = 'http://chat.stackoverflow.com/rooms/info/6/python?id=6&tab=general&users=current'
        req = requests.get(room_url)
        soup = BeautifulSoup(req.text)
        # credit to poke for this one
        cards = [card.get('id')[5:] for card in soup.find(id='room-usercards-container').find_all(class_='usercard')]
        # gets a list of all active users
        return SEUser.query.filter(SEUser.id.in_(cards)).all()

    @classmethod
    def html_load(cls, element, room_id, ts_date=None, update=True):
        """Create a message by id and update it from scraped HTML.

        :param element: message element from Beautiful Soup
        :param room_id: needed for fetching "see full text" messages
        :param ts_date: date parsed from page containing message.  If None, the timestamps are assumed to have the full date, filling in missing fields with today's date.
        :return: instance
        """

        id = int(id_re.search(element['id']).group(1))
        o = cls.get_unique(id=id)

        if not update and o.ts is not None:
            return o

        o.room_id = room_id

        user_url = element.find_previous('div', class_='signature').find('a')['href']
        # don't try to re-cache existing users, since they may be loaded for multiple messages
        o.user = SEUser.se_load(ident=user_url, update=False)

        # Yam it, these are the dumbest timestamps ever.
        # Not every message in the transcript has a timestamp, so we just give all those messages the closest previous timestamp.
        # A timestamp can be:
        # hour:minute period, in which case you need the timestamp from the transcript page, or the current day if this is the starred message list
        # yst hour:minute period, in which case subtract one day
        # weekday hour:minute period, in which case treat today as the last day of the week to calculate and subtract an offset
        # month day hour:minute period, in which case you need to get the year from the transcript or the current day
        # month day 'year hour:minute period, hooray, the only thing wrong with this is the 2 digit year!
        # I know they have the full, seconds resolution, timestamp somewhere, because you can see it when hovering the timestamp in the recently starred list

        # if this is the transcript, the day was parsed and passed in, otherwise it's the chatroom and we start with the current date
        ts_date = ts_date if ts_date is not None else datetime.utcnow().date()
        # find the closest previous timestamp and parse it with a crazy regex to handle all the cases
        ts_data = ts_re.search(element.find_previous('div', class_='timestamp').string).groupdict()
        # at least there's always a time, instead of "5 minutes ago"
        hour = int(ts_data['hour'])
        minute = int(ts_data['minute'])

        if ts_data['month'] is not None:
            # there was a month, so this will replace the start date
            # if there's a year, use strptime to handle 2-digit years as sanely as possible
            # otherwise, use the date we started with to get the year
            year = datetime.strptime(ts_data['year'], '%y').year if ts_data['year'] is not None else ts_date.year
            # get a month's number by name
            month = months.index(ts_data['month'])
            day = int(ts_data['day'])
            # build the new date
            ts_date = date(year, month, day)
        elif ts_data['weekday'] is not None:
            # instead of the date, we got a day of the week in the starred list
            if ts_data['weekday'] == 'yst':
                # or even dumber, we got "yesterday"
                offset = timedelta(-1)
            else:
                # to figure out the offset for a given day relative to the current day
                # remember the days of the week start on monday and are zero based
                # go back 6 days
                # get the number for the day of the week
                # get the number for the current day of the week, treat that as the last day of the week by subtracting from 6
                # add the last day offset to the normal day number, wrapping around if we overflow the week
                offset = timedelta(-6 + ((days.index(ts_data['weekday']) + (6 - ts_date.weekday())) % 7))

            # modify today's date with the offset
            ts_date += offset

        if ts_data['period'] == 'AM' and hour == 12:
            # 12 AM is actually 0 in 24 hour time
            hour = 0
        elif ts_data['period'] == 'PM' and hour != 12:
            # hours after 12 PM are shifted up 12
            hour += 12

        # build a utc timestamp from the date and the time
        o.ts = datetime.combine(ts_date, time(hour, minute))

        if element.find(class_='partial') is not None:
            # this is a "see full text" message, load the full unrendered message
            o.content = requests.get(full_text_url.format(room_id, id)).text
            o.rendered = False
        else:
            # normal full message
            o.content = element.find('div', class_='content').decode_contents().strip()
            o.rendered = True

        stars_elem = element.find('span', class_='stars')
        o.stars = int(stars_elem.find('span', class_='times').string or 0) if stars_elem is not None else 0

        return o
Exemple #14
0
class Salad(IDModel):
    term = db.Column(db.String, nullable=False, unique=True)
    definition = db.Column(db.String, nullable=False)
    position = db.Column(
        db.Integer,
        nullable=False,
        default=lambda: db.session.query(db.func.count(Salad.id)).scalar())
    updated_by_id = db.Column(db.Integer,
                              db.ForeignKey(User.id),
                              nullable=False)

    updated_by = db.relationship(User)

    def __str__(self):
        return self.term

    def compare_value(self):
        return self.term

    @classmethod
    def word_of_the_day(cls, ts=None):
        """Cycle through all words, picking a different one each day.

        Take the number of days since the epoch modulo the total number of items.  Order all words by primary id, and pick the calculated item.

        :param ts: get word for this datetime, or None for now
        :return: instance
        """

        if ts is None:
            ts = datetime.utcnow()

        count = db.session.query(db.func.count(cls.id)).scalar()

        if not count:
            return None

        num = (ts - datetime(1970, 1, 1)).days % count

        return cls.query.order_by(cls.id)[num]

    def move_up(self):
        above = Salad.query.filter(Salad.position == self.position - 1).first()

        if above is not None:
            above.position += 1
            self.position -= 1

    def move_down(self):
        below = Salad.query.filter(Salad.position == self.position + 1).first()

        if below is not None:
            below.position -= 1
            self.position += 1

    def delete(self):
        below = Salad.query.filter(Salad.position > self.position).all()

        for item in below:
            item.position -= 1

        db.session.delete(self)

    @property
    def update_url(self):
        return url_for('salad.update', id=self.id)

    @property
    def move_up_url(self):
        return url_for('salad.move_up', id=self.id)

    @property
    def move_down_url(self):
        return url_for('salad.move_down', id=self.id)

    @property
    def delete_url(self):
        return url_for('salad.delete', id=self.id)

    @property
    def highlight_url(self):
        return url_for('salad.index', highlight=self.term)
Exemple #15
0
    def has_group(self, *groups):
        for group in groups:
            if isinstance(group, str):
                if group in self.groups:
                    return True

            if group in self._groups:
                return True

        return any(sub.has_group(*groups) for sub in self._groups)


group_group = db.Table(
    'group_group',
    db.Column('member_id',
              db.Integer,
              db.ForeignKey(Group.id),
              primary_key=True),
    db.Column('group_id',
              db.Integer,
              db.ForeignKey(Group.id),
              primary_key=True))


class User(UserMixin, SEUser):
    __tablename__ = 'user'

    id = db.Column(db.Integer, db.ForeignKey(SEUser.id), primary_key=True)
    superuser = db.Column(db.Boolean, nullable=False, default=False)

    _groups = db.relationship(Group,
                              lambda: user_group,
Exemple #16
0
    @property
    def detail_url(self):
        return url_for('transcript.detail', id=self)

    @property
    def update_url(self):
        return url_for('transcript.update', id=self.id)

    @property
    def delete_url(self):
        return url_for('transcript.delete', id=self.id)

    @property
    def local_time_url(self):
        query = quote_plus('{} utc in local time'.format(
            self.ts.strftime('%Y-%m-%d %H:%M')))
        return 'http://www.wolframalpha.com/input?i={}'.format(query)


transcript_message = db.Table(
    'transcript_message',
    db.Column('transcript_id',
              db.Integer,
              db.ForeignKey(Transcript.id),
              primary_key=True),
    db.Column('message_id',
              db.Integer,
              db.ForeignKey(ChatMessage.id),
              primary_key=True))