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)
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)
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 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
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)
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
def _tags(cls): return db.relationship(Tag, lambda: cls.tag_assoc, collection_class=set)
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)