class Nick(db.Model): id = db.Column(db.Integer, primary_key=True) contact_id = db.Column(db.Integer, db.ForeignKey('contact.id'), index=True) name = db.Column(db.String(128), unique=True) def __init__(self, name): self.name = name
class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256)) admin = db.Column(db.Boolean) friend = db.Column(db.Boolean) posse_targets = db.relationship('PosseTarget', backref='user', order_by='PosseTarget.id') credentials = db.relationship('Credential', backref='user') # Flask-Login integration def is_authenticated(self): return self.admin def is_active(self): return True def is_anonymous(self): return False def get_id(self): return self.id def __eq__(self, other): return self.id == other.id def __repr__(self): return '<User id={}, name={}>'.format(self.id, self.name)
class Credential(db.Model): type = db.Column(db.String(256), primary_key=True) value = db.Column(db.String(256), primary_key=True) display = db.Column(db.String(256)) # human-readable name user_id = db.Column(db.Integer, db.ForeignKey('user.id')) def __repr__(self): return '<Credential type={}, value={}>'.format(self.type, self.value)
class Tag(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256)) posts = db.relationship('Post', secondary=posts_to_tags) def __init__(self, name): self.name = name def __repr__(self): return self.name
class Contact(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256)) nicks = db.relationship('Nick', backref='contact', cascade='all,delete-orphan') image = db.Column(db.String(512)) url = db.Column(db.String(512)) social = db.Column(JsonType) def __init__(self, **kwargs): self.name = kwargs.get('name') self.url = kwargs.get('url') self.image = kwargs.get('image')
class Attachment(db.Model): id = db.Column(db.Integer, primary_key=True) filename = db.Column(db.String(256)) mimetype = db.Column(db.String(256)) storage_path = db.Column(db.String(256)) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) @property def url(self): return '/'.join((self.post.permalink, 'files', self.filename)) @property def disk_path(self): return os.path.join(current_app.config['UPLOAD_PATH'], self.storage_path)
class Venue(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256)) location = db.Column(JsonType) slug = db.Column(db.String(256)) def update_slug(self, geocode): self.slug = util.slugify(self.name + ' ' + geocode) @property def path(self): return 'venues/{}'.format(self.slug) @property def permalink(self): site_url = get_settings().site_url or 'http://localhost' return '/'.join((site_url, self.path)) def map_image(self, width, height): lat = self.location.get('latitude') lng = self.location.get('longitude') return maps.get_map_image(width, height, 13, [maps.Marker(lat, lng, 'dot-small-pink')])
class Mention(db.Model): id = db.Column(db.Integer, primary_key=True) url = db.Column(db.String(512)) permalink = db.Column(db.String(512)) author_name = db.Column(db.String(128)) author_url = db.Column(db.String(512)) author_image = db.Column(db.String(512)) content = db.Column(db.Text) content_plain = db.Column(db.Text) published = db.Column(db.DateTime) title = db.Column(db.String(512)) syndication = db.Column(JsonType) reftype = db.Column(db.String(32)) rsvp = db.Column(db.String(32)) person_mention = db.Column(db.Boolean) posts = db.relationship('Post', secondary=posts_to_mentions) def __init__(self): self.index = None self.url = None self.permalink = None self.author_name = None self.author_url = None self.author_image = None self.content = None self.content_plain = None self.published = None self.title = None self.reftype = None self.rsvp = None self.person_mention = False self.syndication = [] self._children = [] @property def fragment_id(self): return '{}-{}'.format( self.author_name.lower().replace(' ', '_') if self.author_name else 'unnamed', self.id) @property def title_or_url(self): return self.title or util.prettify_url(self.permalink)
class Context(db.Model): id = db.Column(db.Integer, primary_key=True) url = db.Column(db.String(512)) permalink = db.Column(db.String(512)) author_name = db.Column(db.String(128)) author_url = db.Column(db.String(512)) author_image = db.Column(db.String(512)) content = db.Column(db.Text) content_plain = db.Column(db.Text) published = db.Column(db.DateTime) title = db.Column(db.String(512)) syndication = db.Column(JsonType) def __init__(self, **kwargs): self.url = kwargs.get('url') self.permalink = kwargs.get('permalink') self.author_name = kwargs.get('author_name') self.author_url = kwargs.get('author_url') self.author_image = kwargs.get('author_image') self.content = kwargs.get('content') self.content_plain = kwargs.get('content_plain') self.published = kwargs.get('published') self.title = kwargs.get('title') self.syndication = kwargs.get('syndication', []) @property def title_or_url(self): return self.title or util.prettify_url(self.permalink) def get_slugify_target(self): components = [] if self.author_name: components.append(self.author_name) if self.title: components.append(self.title) elif self.content_plain: components.append(self.content_plain) else: components.append(util.prettify_url(self.permalink or self.url)) return ' '.join(components)
class Setting(db.Model): key = db.Column(db.String(128), primary_key=True) name = db.Column(db.String(256)) value = db.Column(db.Text)
class Post(db.Model): id = db.Column(db.Integer, primary_key=True) path = db.Column(db.String(256)) historic_path = db.Column(db.String(256)) short_path = db.Column(db.String(16)) post_type = db.Column(db.String(64)) draft = db.Column(db.Boolean) deleted = db.Column(db.Boolean) hidden = db.Column(db.Boolean) redirect = db.Column(db.String(256)) tags = db.relationship('Tag', secondary=posts_to_tags) people = db.relationship('Contact', secondary=posts_to_people) friends_only = db.Column(db.Boolean) in_reply_to = db.Column(JsonType) repost_of = db.Column(JsonType) like_of = db.Column(JsonType) bookmark_of = db.Column(JsonType) reply_contexts = db.relationship('Context', secondary=posts_to_reply_contexts) like_contexts = db.relationship('Context', secondary=posts_to_like_contexts) repost_contexts = db.relationship('Context', secondary=posts_to_repost_contexts) bookmark_contexts = db.relationship('Context', secondary=posts_to_bookmark_contexts) title = db.Column(db.String(256)) published = db.Column(db.DateTime, index=True) updated = db.Column(db.DateTime) start_utc = db.Column(db.DateTime) end_utc = db.Column(db.DateTime) start_utcoffset = db.Column(db.Interval) end_utcoffset = db.Column(db.Interval) slug = db.Column(db.String(256)) syndication = db.Column(JsonType) sent_webmentions = db.Column(JsonType) location = db.Column(JsonType) venue_id = db.Column(db.Integer, db.ForeignKey('venue.id')) venue = db.relationship('Venue', uselist=False) mentions = db.relationship('Mention', secondary=posts_to_mentions, order_by='Mention.published') content = db.Column(db.Text) content_html = db.Column(db.Text) attachments = db.relationship('Attachment', backref='post') # reviews item = db.Column(JsonType) rating = db.Column(db.Integer) @classmethod def load_by_id(cls, dbid): return cls.query.get(dbid) @classmethod def load_by_path(cls, path): return cls.query.filter_by(path=path).first() @classmethod def load_by_short_path(cls, path): return cls.query.filter_by(short_path=path).first() @classmethod def load_by_historic_path(cls, path): return cls.query.filter_by(historic_path=path).first() def __init__(self, post_type): self.post_type = post_type self.draft = False self.deleted = False self.hidden = False self.redirect = None self.previous_permalinks = [] self.in_reply_to = [] self.repost_of = [] self.like_of = [] self.bookmark_of = [] self.title = None self.published = None self.start_time = None self.end_time = None self.slug = None self.location = None self.syndication = [] self.sent_webmentions = [] self.audience = [] # public self.mention_urls = [] self.content = None self.content_html = None self.rating = None self.item = None def get_image_path(self): site_url = get_settings().site_url or 'http://localhost' return '/'.join((site_url, self.path, 'files')) def map_image(self, width, height): location = self.location or (self.venue and self.venue.location) if location: lat = location.get('latitude') lng = location.get('longitude') return maps.get_map_image( width, height, 13, [maps.Marker(lat, lng, 'dot-small-blue')]) def get_location_as_geo_uri(self): location = self.location or (self.venue and self.venue.location) if location: lat = location.get('latitude') lng = location.get('longitude') return 'geo:%f,%f' % (lat, lng) @property def start(self): if self.start_utc and self.start_utcoffset: tz = datetime.timezone(self.start_utcoffset) return self.start_utc\ .replace(tzinfo=datetime.timezone.utc)\ .astimezone(tz) return self.start_utc @start.setter def start(self, value): self.start_utc = value and value\ .astimezone(datetime.timezone.utc)\ .replace(tzinfo=None) self.start_utcoffset = value and value.utcoffset() @property def end(self): if self.end_utc and self.end_utcoffset: tz = datetime.timezone(self.end_utcoffset) return self.end_utc\ .replace(tzinfo=datetime.timezone.utc)\ .astimezone(tz) return self.end_utc @end.setter def end(self, value): self.end_utc = value and value\ .astimezone(datetime.timezone.utc)\ .replace(tzinfo=None) self.end_utcoffset = value and value.utcoffset() @property def permalink(self): site_url = get_settings().site_url or 'http://localhost' return '/'.join((site_url, self.path)) @property def shortlink(self): if self.short_path: site_url = get_settings().site_url or 'http://localhost' return '/'.join((site_url, self.short_path)) def _dedupe(self, mentions): all_children = set() mentions = list(mentions) for m in mentions: if m.syndication: m._children = [ n for n in mentions if n.permalink in m.syndication ] all_children.update(m._children) return [m for m in mentions if m not in all_children] @property def likes(self): return self._dedupe(m for m in self.mentions if m.reftype == 'like') @property def reposts(self): return self._dedupe(m for m in self.mentions if m.reftype == 'repost') @property def replies(self): return self._dedupe(m for m in self.mentions if m.reftype == 'reply') @property def rsvps(self): return self._dedupe(m for m in self.mentions if m.reftype == 'rsvp') @property def rsvps_yes(self): return [r for r in self.rsvps if r.rsvp == 'yes'] @property def rsvps_maybe(self): return [r for r in self.rsvps if r.rsvp == 'maybe'] @property def rsvps_no(self): return [r for r in self.rsvps if r.rsvp == 'no'] @property def references(self): return self._dedupe(m for m in self.mentions if m.reftype == 'reference') @property def tweet_id(self): for url in self.syndication: match = util.TWITTER_RE.match(url) if match: return match.group(2) def _fill_in_action_handler(self, url): return url.replace('{url}', urllib.parse.quote_plus(self.permalink)) @property def reply_url(self): handlers = session.get('action-handlers', {}) handler = handlers.get('reply') if handler: return self._fill_in_action_handler(handler) tweet_id = self.tweet_id if tweet_id: return TWEET_INTENT_URL.format(tweet_id) return '#' @property def retweet_url(self): handlers = session.get('action-handlers', {}) handler = handlers.get('repost') if handler: return self._fill_in_action_handler(handler) tweet_id = self.tweet_id if tweet_id: return RETWEET_INTENT_URL.format(tweet_id) return '#' @property def favorite_url(self): handlers = session.get('action-handlers', {}) handler = handlers.get('favorite') or handlers.get('like') if handler: return self._fill_in_action_handler(handler) tweet_id = self.tweet_id if tweet_id: return FAVORITE_INTENT_URL.format(tweet_id) return '#' @property def location_url(self): if (self.location and 'latitude' in self.location and 'longitude' in self.location): return OPEN_STREET_MAP_URL.format(self.location['latitude'], self.location['longitude']) @property def mf2_type(self): if self.post_type == 'review': return 'h-review' if self.post_type == 'event': return 'h-event' return 'h-entry' @property def title_or_fallback(self): """Feeds and <title> attributes require a human-readable title, even for posts that do not have an explicit title. Try here to create a reasonable one. """ def format_context(ctx): if ctx.title and ctx.author_name: return '“{}” by {}'.format(ctx.title, ctx.author_name) if ctx.title: return ctx.title if ctx.author_name: return 'a post by {}'.format(ctx.author_name) return util.prettify_url(ctx.permalink) if self.title: return self.title if self.post_type == 'checkin' and self.venue: return 'Checked in to {}'.format(self.venue.name) if self.repost_contexts: return 'Shared {}'.format(', '.join( map(format_context, self.repost_contexts))) if self.like_contexts: return 'Liked {}'.format(', '.join( map(format_context, self.like_contexts))) if self.bookmark_contexts: return 'Bookmarked {}'.format(', '.join( map(format_context, self.bookmark_contexts))) if self.content: return util.format_as_text(self.content) return 'A {} from {}'.format(self.post_type, self.published) def generate_slug(self): if self.title: title = self.title if self.post_type == 'event' and self.start: title = self.start.strftime('%b %d') + ' ' + title return util.slugify(title) if self.item: item_name = self.item.get('name') if item_name: if 'author' in self.item: item_name += ' by ' + self.item.get('author') return util.slugify('review of ' + item_name) content_plain = util.format_as_text(self.content or '', None) if content_plain: return util.slugify(content_plain, 48) or 'untitled' if self.post_type == 'checkin' and self.venue: return util.slugify( 'checked into ' + self.venue.name + ' ' + content_plain, 48) for ctxs, prefix in ((self.bookmark_contexts, 'bookmark-of-'), (self.like_contexts, 'like-of-'), (self.repost_contexts, 'repost-of-'), (self.reply_contexts, 'reply-to-')): if ctxs: return util.slugify(prefix + ctxs[0].get_slugify_target(), 48) return 'untitled' def add_syndication_url(self, url): """Add a new URL to the list of syndicated posts. We cannot do this directly because syndication is a JsonType that cannot detect modifications to itself. """ # JsonType cannot detect changes to itself. Have to create a new list # to modify it new_synd = list(self.syndication) new_synd.append(url) self.syndication = new_synd def __repr__(self): if self.title: return 'post:{}'.format(self.path) else: return 'post:{}'.format(self.path)
class PosseTarget(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) micropub_endpoint = db.Column(db.String(256)) access_token = db.Column(db.String(1024)) style = db.Column(db.String(32)) uid = db.Column(db.String(256)) name = db.Column(db.String(256)) user_name = db.Column(db.String(256)) user_url = db.Column(db.String(256)) user_photo = db.Column(db.String(256)) service_photo = db.Column(db.String(256)) service_url = db.Column(db.String(256)) service_name = db.Column(db.String(256))
class Settings: def __init__(self): for s in Setting.query.all(): setattr(self, s.key, s.value) def get_settings(): settings = g.get('rw_settings', None) if settings is None: g.rw_settings = settings = Settings() return settings posts_to_mentions = db.Table( 'posts_to_mentions', db.Model.metadata, db.Column('post_id', db.Integer, db.ForeignKey('post.id'), index=True), db.Column('mention_id', db.Integer, db.ForeignKey('mention.id'), index=True)) posts_to_reply_contexts = db.Table( 'posts_to_reply_contexts', db.Model.metadata, db.Column('post_id', db.Integer, db.ForeignKey('post.id'), index=True), db.Column('context_id', db.Integer, db.ForeignKey('context.id'), index=True)) posts_to_repost_contexts = db.Table( 'posts_to_repost_contexts', db.Model.metadata,