class Confirm(db.Model): """ Holds the data for a confirm action. :param action: The key of the wanted action. :param expires: Defines when the confirm should expire. This is only accurate to one day. This may either be a datetime or date, a timedelta or an integer, which is then considered the time until expiration in days. If the link is called after it is expired, the user is rejected. :param data: Data which is passed to the registered function when the link is accessed. It is saved pickled, so make sure your object is pickleable. """ __tablename__ = 'core_confirm' __mapper_args__ = { 'extension': ConfirmMapperExtension(), } query = db.session.query_property(ConfirmQuery) _key_length = 32 _key_chars = string.ascii_letters + string.digits id = db.Column(db.Integer, primary_key=True) key = db.Column(db.Unicode(32), unique=True) action = db.Column(db.Unicode(40)) data = db.Column(db.PickleType) expires = db.Column(db.Date) def __init__(self, action, data, expires): if action not in CONFIRM_ACTIONS: raise KeyError('Action %r is not registered.' % action) if isinstance(expires, (datetime, date)): self.expires = expires elif isinstance(expires, timedelta): self.expires = date.today() + expires else: self.expires = date.today() + timedelta(days=expires) self.action = action self.data = data def __repr__(self): return '<Confirm %s %s>' % (self.action, getattr(self, 'key', '')) @classmethod def _make_key(cls): return u''.join( random.choice(cls._key_chars) for _ in xrange(cls._key_length)) @property def url(self): return href('portal/confirm', key=self.key, _external=True) @property def is_expired(self): return self.expires < date.today()
class Group(db.Model): __tablename__ = 'core_group' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode(40), unique=True) parents = db.relationship( 'Group', secondary=group_group, backref=db.backref('children', collection_class=set, lazy='subquery'), primaryjoin=(id == group_group.c.group_id), secondaryjoin=(group_group.c.parent_id == id), foreign_keys=[group_group.c.group_id, group_group.c.parent_id], collection_class=set, cascade='all') def get_parents(self): if not self.parents: return parents = [] for group in self.parents: parents.append(group) if group.parents: parents.extend(group.get_parents()) return parents @cached_property def grant_parent(self): return self.get_parents()[-1] def get_url_values(self, action='view'): values = { 'view': 'portal/group', } return values[action], {'name': self.name}
class UserProfile(db.Model): """The profile for an user. The user profile contains various information about the user e.g the real name, his website and various contact information. This model provides basic fields but is extendable to provide much more information if required. """ __tablename__ = 'core_userprofile' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.ForeignKey(User.id)) user = db.relationship(User, backref=db.backref('profile', uselist=False, lazy='joined', innerjoin=True)) # profile properties. We implement them here to get a better documented # and tested model structure. Feel free to advance the attributes! # personal attributes real_name = db.Column(db.Unicode(200)) website = db.Column(db.Unicode(200)) location = db.Column(db.Unicode(200)) interests = db.Column(db.Unicode(200)) occupation = db.Column(db.Unicode(200)) signature = db.Column(db.Text) # communication channels jabber = db.Column(db.Unicode(200)) skype = db.Column(db.Unicode(200)) def get_url_values(self, action='view'): values = { 'view': ('portal/profile', { 'username': self.user.username }), 'edit': ('portal/profile_edit', {}), } return values[action][0], values[action][1] def clear(self): self.real_name = u'' self.website = u'' self.location = u'' self.intersts = u'' self.occupation = u'' self.signature = u''
class Tag(db.Model, SerializableObject): __tablename__ = 'core_tag' __mapper_args__ = {'extension': db.SlugGenerator('slug', 'name')} query = db.session.query_property(TagQuery) #: serializer attributes object_type = 'core.tag' public_fields = ('id', 'name', 'slug') id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode(20), nullable=False, index=True) slug = db.Column(db.Unicode(20), nullable=False, unique=True) #: Number of items tagged tagged = db.Column(db.Integer, nullable=False, default=0) #: Flag if the tag is public or internal. If a tag is not public #: it's probably used by internal functions to ensure special #: permission flags. It's not shown in any public interface (e.g tag-cloud) public = db.Column(db.Boolean, nullable=False, default=True) def __unicode__(self): return self.name @db.validates('name') def validate_tag(self, key, tag): if not tag_re.match(tag): raise ValueError(u'Invalid tag name %s' % tag) return tag def get_url_values(self, action='view'): values = { 'view': 'portal/tag', 'edit': 'portal/tag_edit', 'delete': 'portal/tag_delete', } return values[action], {'slug': self.slug}
class User(db.Model, SerializableObject): __tablename__ = 'core_user' __mapper_args__ = {'extension': AutomaticUserProfile()} query = db.session.query_property(UserQuery) # serializer properties object_type = 'inyoka.user' public_fields = ('id', 'username', 'pw_hash', 'status', 'real_name') #: The internal ID of the user. Even if an external Auth system is #: used, we're storing the users a second time internal so that we #: can easilier work with relations. id = db.Column(db.Integer, primary_key=True) #: The username of the user. For external auth systems it makes a #: lot of sense to allow the user to chose a name on first login. username = db.Column(db.Unicode(40), unique=True) #: The email of the user. If an external auth system is used, the #: login code should update that information automatically on login email = db.Column(db.Unicode(250), index=True, unique=True) #: The password hash. This might not be used by every auth system. #: the OpenID auth for example does not use it at all. But also #: external auth systems might not store the password here. pw_hash = db.Column(db.Unicode(60)) #: When the user registered himself date_joined = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) #: When the user recently joined the webpage last_login = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) #: The status of the user. 0: inactive, 1: normal, 2: banned, 3: deleted _status = db.Column('status', db.Integer, nullable=False, default=0) groups = db.relationship(Group, secondary=user_group, backref=db.backref('users', lazy='dynamic')) def __init__(self, *args, **kwargs): # we just ignore applied pw_hashes so we calculate those ourselfs kwargs.pop('pw_hash', None) self.set_password(kwargs.pop('password', u'')) db.Model.__init__(self, *args, **kwargs) def set_password(self, raw_password): """Set a new sha1 generated password hash""" salt = u'%05x' % random.getrandbits(20) hsh = get_hexdigest(salt, raw_password) self.pw_hash = u'%s$%s' % (salt, hsh) def check_password(self, raw_password): """ Returns a boolean of whether the raw_password was correct. """ salt, hsh = self.pw_hash.split(u'$') return hsh == get_hexdigest(salt, raw_password) def _set_status(self, status): self._status = USER_STATUS_MAP[status] def _get_status(self): return USER_STATUS_MAP[self._status] status = db.synonym('_status', descriptor=property(_get_status, _set_status)) is_active = property(lambda self: self.status == u'normal') @property def display_name(self): return self.username @property def is_anonymous(self): name = ctx.cfg['anonymous_name'] return self.username == name def subscribed(self, type, subject_id=None): return subscribed(type, self, subject_id) def get_url_values(self, action='profile'): if action == 'profile': return 'portal/profile', {'username': self.username} def deactivate(self): """Deactivates the user for ever. Use with care!! This method does not commit the changes to the database. """ self._status = 3 self.profile.clear() def __repr__(self): i = '#%d' % self.id if self.id else '[no id]' return '<User %s %r>' % (i, self.username) def __unicode__(self): return self.display_name
class Subscription(db.Model): __tablename__ = 'core_subscription' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.ForeignKey(User.id), nullable=False) type_name = db.Column('type', db.Unicode(20), nullable=False) count = db.Column(db.Integer, nullable=False, default=0) subject_id = db.Column(db.Integer) first_unread_object_id = db.Column(db.Integer) user = db.relationship(User) unread_object_ids = association_proxy( '_unread_object_ids', 'object_id', creator=_create_subscriptionunreadobjects_by_object_id) @staticmethod def new(object, action): """ Sends notifications for a new object, saves it as `first_unread_object` or in `unread_objects` (depending on the type's mode) and increments the unread count. Must be called whenever a new object is created or moved to the scope of a SubscriptionType (e.g. when a topic is moved to another forum). :param object: The added object. :param action: The action which lead to the creation of the object. It is used to select the SubscriptionTypes that need to be considered and the action's :meth:`notify` method is called. """ #XXX: if `object` is removed before the task is run, subscriptions are # marked as notified though the user didn't receive a notification if isinstance(action, basestring): action = SubscriptionAction.by_name(action) #: store ids of Subscription objects for which we need to notify subscriptions = [] for t in SubscriptionType.by_action(action): subjects = t.get_subjects(object) if not subjects: continue subject_ids = [getattr(s, 'id', s) for s in subjects] base_cond = (Subscription.type_name == t.name) if t.subject_type is not None: base_cond &= Subscription.subject_id.in_(subject_ids) if t.mode == 'sequent': #: increment unread count where there already are unread objects cond = base_cond & (Subscription.count > 0) q = db.update(Subscription.__table__, cond, {'count': Subscription.count + 1}) db.session.execute(q) db.session.commit() #: then set the first unread object and notify the user #: where there were no new objects since the last visit cond = base_cond & (Subscription.count == 0) for s in Subscription.query.filter(cond): subscriptions.append(s.id) s.first_unread_object_id = object.id s.count = 1 db.session.commit() if t.mode == 'multiple': for s in Subscription.query.filter(base_cond): subscriptions.append(s.id) s.unread_object_ids.add(object.id) s.count = len(s.unread_object_ids) db.session.commit() from inyoka.core import tasks tasks.send_notifications(object, action.name, subscriptions) @staticmethod def accessed(user, **kwargs): """ Mark subscriptions as read. This means to remove objects from the `unread_objects` or unset `first_unread_object` (depending on the type's mode) and decrement the unread count. Must be called whenever an object is accessed. :param object: Mark all subscriptions with this object as read. :param subject: Mark all subscriptions with this subject as read. """ # enforce usage of keywords object = kwargs.pop('object', None) subject = kwargs.pop('subject', None) assert not kwargs, 'Invalid Arguments %r' % kwargs.keys() if object is not None: for t in SubscriptionType.by_object_type(type(object)): subjects = t.get_subjects(object) if not subjects: continue subject_ids = [getattr(s, 'id', s) for s in subjects] cond = ((Subscription.type_name == t.name) & (Subscription.user == user)) if t.subject_type is not None: cond &= Subscription.subject_id.in_(subject_ids) subscriptions = Subscription.query.filter(cond).all() for s in subscriptions: if t.mode == 'sequent': s.first_unread_object_id = None s.count = 0 elif t.mode == 'multiple': try: s.unread_object_ids.remove(object.id) except KeyError: pass else: s.count -= 1 db.session.commit() if subject is not None: for t in SubscriptionType.by_subject_type(type(subject)): try: s = Subscription.query.filter_by(type_name=t.name, subject_id=subject.id, user=user).one() except db.NoResultFound: continue s.count = 0 if t.mode == 'sequent': s.first_unread_object_id = None elif t.mode == 'multiple': s.unread_object_ids = [] db.session.commit() @staticmethod def subscribe(user, type_, subject=None): """ Safely subscribe a user to a subject. Returns False if the Subscription already exists, else True. :param user: A user object or a user id. :param type_: The subscription type to be used. May be a subclass of :class:`SubscriptionType` or a name of a subscription type. :param subject: The subject to be used (id or instance). May be None if the type has not ``subject_type``. """ if isinstance(type_, basestring): type_ = SubscriptionType.by_name(type_) subject_type = type_ and type_.subject_type or type(None) if not isinstance(subject, subject_type): raise ValueError('subject (%r) does not match the subject_type ' '(%r) of given SubscriptionType' % (subject, subject_type)) subject_id = None if subject is None else subject.id args = { 'user': user, 'type_name': type_.name, 'subject_id': subject_id, } if Subscription.query.filter_by(**args).count(): return False Subscription(**args) db.session.commit() return True @staticmethod def unsubscribe(user_or_subscription, type_=None, subject=None): """ Safely unsubscribe a user from a subject. Returns False if the Subscription did not exist, else True. :param user_or_subscription: A user object or a user id. May also be a Subscription object, in this case the other parameters must not be given. :param type_: The subscription type to be used. May be a subclass of :class:`SubscriptionType` or a name of a subscription type. :param subject: The subject to be used (id or instance). May be None if the type has not ``subject_type``. """ if isinstance(user_or_subscription, Subscription): assert type_ is None and subject is None db.session.delete(user_or_subscription) db.session.commit() return True else: user = user_or_subscription if isinstance(type_, basestring): type_ = SubscriptionType.by_name(type_) s = Subscription.query.filter_by(user=user, type_name=type_.name, subject_id=getattr( subject, 'id', subject)).all() if not len(s): return False if len(s) > 1: raise ValueError('Duplicate found!') db.session.delete(s[0]) db.session.commit() return True @property def type(self): return SubscriptionType.by_name(self.type_name) @property def subject(self): if None not in (self.type.subject_type, self.subject_id): return self.type.subject_type.query.get(self.subject_id)
class Storage(db.Model): __tablename__ = 'core_storage' key = db.Column(db.Unicode(200), primary_key=True, index=True) value = db.Column(db.PickleType)
class Cache(db.Model): __tablename__ = 'core_cache' key = db.Column(db.Unicode(60), primary_key=True, nullable=False) value = db.Column(db.PickleType, nullable=False) expires = db.Column(db.DateTime, nullable=False)