class Revisions(BaseMixin, db.Model): """ Collection of revisions of content. """ __tablename__ = 'revisions' #: All revisions of the content history = db.relationship( ContentRevision, primaryjoin='ContentRevision.parent_id == Revisions.id', order_by=ContentRevision.id.desc, backref=db.backref('parent', cascade='all')) #: Latest published revision published_id = db.Column(db.Integer, db.ForeignKey('content_revision.id'), nullable=True) published = db.relationship(ContentRevision, post_update=True, primaryjoin=published_id == ContentRevision.id) #: Latest draft revision draft_id = db.Column(db.Integer, db.ForeignKey('content_revision.id'), nullable=True) draft = db.relationship(ContentRevision, post_update=True, primaryjoin=draft_id == ContentRevision.id)
class ParticipantList(ContentMixin, Node): __tablename__ = 'participant_list' source = db.Column(db.Unicode(80), nullable=False, default=u'') sourceid = db.Column(db.Unicode(80), nullable=False, default=u'') api_key = db.Column(db.Unicode(80), nullable=False, default=u'') participant_template = db.Column(db.Unicode(80), nullable=False, default=u'') def purge(self): """ Discard all participants. """ self.participants = [] def has_user(self, user): return Participant.query.filter_by(participant_list=self).filter_by( user=user).first() is not None def url_for(self, action='view'): if action in ['sync', 'list']: return url_for('node_action', website=self.folder.website.name, folder=self.folder.name, node=self.name, action=action) else: return super(ParticipantList, self).url_for(action)
class Data(NodeMixin, Node): __tablename__ = 'data' data = db.Column(JsonDict) def as_json(self): result = super(NodeMixin, self).as_json() result['data'] = self.data return result def import_from(self, data): super(NodeMixin, self).import_from(data) self.data = data['data'] # Facilitate easier access to data def __len__(self): return len(self.data) def __iter__(self): return self.data.iterkeys() def __contains__(self, item): return item in self.data def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def __delitem__(self, key): del self.data[key]
class ContentRevision(BaseMixin, db.Model): """ A single revision of any piece of content. """ __tablename__ = 'content_revision' parent_id = db.Column(db.Integer, db.ForeignKey('revisions.id', use_alter=True, name='fk_content_revision_parent_id'), nullable=False) #: Previous revision previous_id = db.Column(db.Integer, db.ForeignKey('content_revision.id'), nullable=True) previous = db.relationship('ContentRevision', remote_side='ContentRevision.id', uselist=False) #: User who made this content revision user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, default=default_user_id) user = db.relationship(User) #: Title of the current revision title = db.Column(db.Unicode(250), nullable=False) #: Abstract that is shown in summaries. Plain text. description = db.Column(db.UnicodeText, nullable=False, default=u'') #: Page content. Rich text. _content = db.Column('content', db.UnicodeText, nullable=False, default=u'') #: Template with which this page will be rendered template = db.Column(db.Unicode(80), nullable=False, default=u'') def __init__(self, **kwargs): super(ContentRevision, self).__init__(**kwargs) if self.previous: # Copy over content from the previous revision if not self.user: self.user = self.previous.user if not self.title: self.title = self.previous.title if not self.description: self.description = self.previous.description if not self._content: self._content = self.previous._content if not self.template: self.template = self.previous.template @property def content(self): return Markup(self._content) @content.setter def content(self, value): self._content = value content = db.synonym('_content', descriptor=content)
class Redirect(NodeMixin, Node): __tablename__ = 'redirect' redirect_url = db.Column(db.Unicode(250), nullable=False) def as_json(self): result = super(Redirect, self).as_json() result.update({'redirect_url': self.redirect_url}) return result def import_from(self, data): super(Redirect, self).import_from(data) self.redirect_url = data['redirect_url']
class FunnelLink(ContentMixin, Node): __tablename__ = 'funnel_link' funnel_name = db.Column(db.Unicode(80), nullable=False) def as_json(self): result = super(FunnelLink, self).as_json() result.update({'funnel_name': self.funnel_name}) return result def import_from(self, data): super(FunnelLink, self).import_from(data) self.funnel_name = data['funnel_name'] def _data(self): if not hasattr(self, '_data_cached'): # Get JSON and cache locally try: r = requests.get('http://funnel.hasgeek.com/%s/json' % self.funnel_name) data = r.json() if callable(r.json) else r.json sectionmap = dict([(s['title'], s['name']) for s in data['sections']]) for proposal in data['proposals']: proposal['submitted'] = parse_isoformat( proposal['submitted']) proposal['section_name'] = sectionmap.get( proposal['section']) v = proposal['votes'] proposal['votes'] = '+%d' % v if v > 0 else '%d' % v self._data_cached = data except ConnectionError: self._data_cached = { 'proposals': [], 'sections': [], 'space': {}, } return self._data_cached def proposals_mapping(self): if not hasattr(self, '_dict_cached'): self._dict_cached = dict([(p['id'], p) for p in self.proposals()]) return self._dict_cached def sections(self): # Get data from Funnel and cache locally return self._data()['sections'] def proposals(self): return self._data()['proposals'] def confirmed(self): return [p for p in self._data()['proposals'] if p['confirmed']]
class MapItem(BaseScopedNameMixin, db.Model): __tablename__ = 'map_item' map_id = db.Column(None, db.ForeignKey('map.id'), nullable=False) parent = db.synonym('map') url = db.Column(db.Unicode(250), nullable=True) latitude = db.Column(db.Numeric(7, 4), nullable=False) longitude = db.Column(db.Numeric(7, 4), nullable=False) zoomlevel = db.Column(db.Integer, nullable=True) marker = db.Column(db.Unicode(80), nullable=True) seq = db.Column(db.Integer, nullable=False) __table_args__ = (db.UniqueConstraint('map_id', 'name'),) @classmethod def get_or_new(cls, map, name=None): item = cls.query.filter_by(map=map, name=name).first() if item is None: item = cls(map=map, name=name) db.session.add(item) return item
class ListItem(BaseMixin, db.Model): __tablename__ = 'list_item' list_id = db.Column(None, db.ForeignKey('list.id'), nullable=False) name = db.Column(db.Unicode(80), nullable=True) title = db.Column(db.Unicode(250), nullable=True) url = db.Column(db.Unicode(250), nullable=True) node_id = db.Column(None, db.ForeignKey('node.id'), nullable=True) node = db.relationship(Node, backref=db.backref('lists', cascade='all, delete-orphan')) seq = db.Column(db.Integer, nullable=False) __table_args__ = ( db.UniqueConstraint( 'list_id', 'name'), # name, if present, must be unique in this list db.UniqueConstraint('list_id', 'node_id') # A node can only be in a list once ) @classmethod def get_or_new(cls, list, name=None, node=None): if name is None and node is None: return query = cls.query.filter_by(list=list) if name: query = query.filter_by(name=name) if node: query = query.filter_by(node=node) item = query.first() if item: return item else: item = cls(list=list, name=name, node=node) db.session.add(item) return item
class ContentMixin(NodeMixin): is_published = db.Column('published', db.Boolean, default=False, nullable=False, index=True) @declared_attr def revisions_id(cls): return db.Column(db.Integer, db.ForeignKey('revisions.id'), nullable=False) @declared_attr def revisions(cls): return db.relationship(Revisions, uselist=False) def __init__(self, **kwargs): self.revisions = Revisions() super(ContentMixin, self).__init__(**kwargs) def last_revision(self): revision = self.revisions.draft or self.revisions.published if revision is None: revision = ContentRevision.query.filter_by( parent=self.revisions).order_by( db.desc('id')).limit(1).first() return revision def publish(self, revision=None): """ Publish the given revision, or the latest draft. """ if revision is not None and revision.parent is not self.revisions: raise ValueError("This revision isn't from this content document.") if revision is None: revision = self.revisions.draft if revision is not None: if not self.revisions.published: # TODO: Expose this for user editing. Don't assume current self.published_at = datetime.utcnow() self.revisions.published = revision self.is_published = True # Update node title from the revision self.title = revision.title # If this was the latest draft, there's no longer a latest draft if revision is self.revisions.draft: self.revisions.draft = None def unpublish(self): """ Withdraw the published version and go back to being a draft. """ self.revisions.published = None revision = ContentRevision.query.filter_by( parent=self.revisions).order_by(db.desc('id')).limit(1).first() self.revisions.draft = revision self.title = revision.title self.is_published = False def revise(self, revision=None): """Create and return a new revision.""" if revision and revision.parent is not self.revisions: raise ValueError("This revision isn't from this content document.") # Make and return a new revision based on the given revision or the latest draft if revision is None: # If parent is still None after this, there was no parent revision = self.last_revision() new_revision = ContentRevision(parent=self.revisions, previous=revision) self.revisions.draft = new_revision db.session.add(new_revision) return new_revision @property def template(self): if self.revisions.published: return self.revisions.published.template else: return self.revisions.draft.template @property def description(self): # TODO: Only show drafts to a logged in, authorized user if self.is_published: return self.revisions.published.description else: return self.revisions.draft.description @property def content(self): # TODO: Only show drafts to a logged in, authorized user if self.is_published: return self.revisions.published.content else: return self.revisions.draft.content def __repr__(self): return u'<%s %s/%s "%s" at %s>' % ( self.__tablename__.title(), self.folder.name, self.name or '(index)', self.title, self.folder.website.name) def as_json(self): result = super(ContentMixin, self).as_json() revision = self.revisions.draft or self.revisions.published if not revision: return result result.update({ 'title': revision.title, 'description': revision.description, 'content': revision.content, 'template': revision.template, 'revision_created_at': revision.created_at.isoformat() + 'Z', 'revision_updated_at': revision.updated_at.isoformat() + 'Z', 'is_published': self.is_published, }) return result def import_from(self, data): super(ContentMixin, self).import_from(data) last = self.last_revision() if last and last.updated_at >= parse_isoformat( data['revision_updated_at']): # Don't import if data is older than or the same as the last revision return revision = self.revise() revision.title = data['title'] revision.description = data['description'] revision.content = data['content'] revision.template = data['template'] def url_for(self, action='view'): if action == 'publish': return url_for('node_publish', website=self.folder.website.name, folder=self.folder.name, node=self.name) elif action == 'unpublish': return url_for('node_unpublish', website=self.folder.website.name, folder=self.folder.name, node=self.name) else: return super(ContentMixin, self).url_for(action)
def revisions_id(cls): return db.Column(db.Integer, db.ForeignKey('revisions.id'), nullable=False)
class Participant(BaseMixin, db.Model): __tablename__ = 'participant' #: List that this participant is in participant_list_id = db.Column(None, db.ForeignKey('participant_list.id'), nullable=False) #: Datetime when this participant's record was created upstream datetime = db.Column(db.DateTime, nullable=True) #: Ticket no, the reference key ticket = db.Column(db.Unicode(80), nullable=True, unique=True) #: Participant's name fullname = db.Column(db.Unicode(80), nullable=True) #: Unvalidated email address email = db.Column(db.Unicode(80), nullable=True) #: Unvalidated phone number phone = db.Column(db.Unicode(80), nullable=True) #: Unvalidated Twitter id twitter = db.Column(db.Unicode(80), nullable=True) #: Ticket type, if the registration system had multiple tickets ticket_type = db.Column(db.Unicode(80), nullable=True) #: Job title jobtitle = db.Column(db.Unicode(80), nullable=True) #: Company company = db.Column(db.Unicode(80), nullable=True) #: Participant's city city = db.Column(db.Unicode(80), nullable=True) #: T-shirt size tshirt_size = db.Column(db.Unicode(4), nullable=True) #: Link to the user record user_id = db.Column(None, db.ForeignKey('user.id'), nullable=True) user = db.relationship(User) #: Access key for connecting to the user record (nulled when linked) access_key = db.Column(db.Unicode(44), nullable=True, default=newsecret, unique=True) #: Is listed in the public directory is_listed = db.Column(db.Boolean, default=False, nullable=False) #: Data fields the participant has chosen to reveal in public fields_directory = db.Column(db.Unicode(250), nullable=False, default=u'fullname company') #: Data fields the participant has chosen to reveal via ContactPoint fields_contactpoint = db.Column( db.Unicode(250), nullable=False, default=u'fullname email phone twitter jobtitle company city') participant_list = db.relationship(ParticipantList, backref=db.backref( 'participants', order_by=fullname, cascade='all, delete-orphan')) parent = db.synonym('participant_list')