Ejemplo n.º 1
0
class Fileview(Model):
    user_id = Property()
    sharedfile_id = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        return super(Fileview, self).save(*args, **kwargs)

    @classmethod
    def last(cls):
        return cls.get("1 order by id desc limit 1")

    @classmethod
    def sharedfile_ids(cls, after_id=None):
        """
        Return sharedfile_ids that have had views logged to them.
        
        Limit results to fileview's > passed in after_id.
        """
        sql = "select sharedfile_id from fileview "
        if after_id:
            sql = sql + " where id > %s" % int(after_id)
        sql += " group by sharedfile_id"
        return [result['sharedfile_id'] for result in cls.query(sql)]
Ejemplo n.º 2
0
class Shakesharedfile(Model):
    shake_id = Property()
    sharedfile_id = Property()
    deleted = Property(default=0)
    created_at  = Property()
    
    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Shakesharedfile, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def delete(self):
        self.deleted =1
        self.save()
Ejemplo n.º 3
0
class Promotion(ModelQueryCache, Model):
    # Name of promotion
    name = Property()

    # Shake this promotion relates to
    # (used for a profile pic and link to
    # the promotion shake)
    promotion_shake_id = Property()

    # Number of Pro membership months this
    # promotion is good for
    membership_months = Property()

    # a link to a web page about this promotion
    promotion_url = Property()

    # date that the promotion expires on
    expires_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        return super(Promotion, self).save(*args, **kwargs)

    def shake(self):
        return Shake.get('id=%s and deleted=0', self.promotion_shake_id)

    @classmethod
    def active(cls):
        return cls.where("expires_at > now() order by rand()")
Ejemplo n.º 4
0
class Favorite(Model):
    user_id = Property()
    sharedfile_id = Property()
    deleted = Property(default=0)
    created_at = Property()
    updated_at = Property()
    
    def user(self):
        return user.User.get('id = %s', self.user_id)
    
    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Favorite, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def pretty_created_at(self):
        """
        A friendly version of the created_at date.
        """
        return pretty_date(self.created_at)
Ejemplo n.º 5
0
class CommentLike(Model):
    user_id = Property()
    comment_id = Property()
    deleted = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(CommentLike, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def on_create(self):
        u = user.User.get('id = %s', self.user_id)
        n = notification.Notification.new_comment_like(self, u)
        n.save()
Ejemplo n.º 6
0
class ScriptLog(Model):
    name = Property(default='')
    result = Property(default='')
    success = Property(default=0)
    started_at = Property()
    finished_at = Property()

    def start_running(self):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self.started_at = datetime.utcnow()

    def save(self, *args, **kwargs):
        self._set_dates()
        return super(ScriptLog, self).save(*args, **kwargs)

    def _set_dates(self):
        if self.id is None or self.created_at is None:
            self.finished_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    @classmethod
    def last_successful(cls, name):
        return cls.get("name = %s and success = 1 order by id desc limit 1",
                       name)
Ejemplo n.º 7
0
class Magicfile(Model):
    sharedfile_id = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        super(Magicfile, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def sharedfile(self):
        """
        Returns the associated sharedfile object.
        """
        if not bool(self.sharedfile_id):
            return None
        return models.Sharedfile.get("id = %s", self.sharedfile_id)

    @classmethod
    def sharedfiles_paginated(self,
                              before_id=None,
                              after_id=None,
                              per_page=10):
        """
        Returns a list of "Magicfile" Sharedfiles, with the "magicfile_id"
        property set for each object.
        """
        constraint_sql = ""
        order = "desc"

        if before_id:
            constraint_sql = "AND magicfile.id < %s" % (int(before_id))
        elif after_id:
            order = "asc"
            constraint_sql = "AND magicfile.id > %s" % (int(after_id))

        select = """SELECT sharedfile.*, magicfile.id AS magicfile_id FROM magicfile
                    LEFT JOIN sharedfile
                    ON magicfile.sharedfile_id = sharedfile.id
                    WHERE sharedfile.deleted = 0
                    %s
                    ORDER BY magicfile.id %s LIMIT 0, %s""" % (constraint_sql,
                                                               order, per_page)
        files = models.Sharedfile.object_query(select)
        if order == "asc":
            files.reverse()
        return files
Ejemplo n.º 8
0
class Externalservice(Model):
    user_id = Property()
    service_id = Property()
    screen_name = Property()
    type = Property()
    service_key = Property()
    service_secret = Property()
    deleted = Property(default=0)
    created_at = Property()
    updated_at = Property()

    TWITTER = 1
    FACEBOOK = 2

    @staticmethod
    def by_user(user, type):
        """
        Returns an external service for a user and type.
        """
        if not type:
            return None

        if not user:
            return None

        return Externalservice.get("user_id = %s and type = %s", user.id, type)

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Externalservice, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def find_mltshp_users(self):
        """
        Find mltshp Users whose external service (twitter, etc.) friends overlap
        with current user. 
        """
        sql = """ select user.* from user
                  left join externalservice
                  on externalservice.user_id = user.id
                  left join external_relationship
                  on external_relationship.service_id = externalservice.service_id
                  where external_relationship.user_id = %s
                  and external_relationship.service_type = %s
                  order by id desc"""
        return user.User.object_query(sql, self.user_id, self.TWITTER)
Ejemplo n.º 9
0
class Subscription(Model):
    user_id = Property()
    shake_id = Property()
    deleted = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Subscription, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def on_create(self):
        sub_shake = shake.Shake.get('id=%s and deleted=0', self.shake_id)
        sub_user = user.User.get('id = %s and deleted=0', self.user_id)
        shake_owner = user.User.get('id = %s and deleted=0', sub_shake.user_id)

        shared_files = sub_shake.sharedfiles()
        for sf in shared_files:
            existing_post = post.Post.where(
                'user_id = %s and sourcefile_id = %s', sub_user.id,
                sf.source_id)
            seen = 0
            if existing_post:
                seen = 1
            new_post = post.Post(user_id=sub_user.id,
                                 sharedfile_id=sf.id,
                                 sourcefile_id=sf.source_id,
                                 seen=seen,
                                 shake_id=sub_shake.id)
            new_post.save()
            new_post.created_at = sf.created_at
            new_post.save()

    def shake(self):
        return shake.Shake.get("id = %s", self.shake_id)
Ejemplo n.º 10
0
class Tag(ModelQueryCache, Model):
    name = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Tag, self).save(*args, **kwargs)

    def _set_dates(self):
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def path(self):
        return '/%s' % self.name.lower()

    def sharedfiles_paginated(self, per_page=10, since_id=None, max_id=None):
        """
        Pulls a tags's timeline, can key off and go backwards (max_id) and forwards (since_id)
        in time to pull the per_page amount of posts.
        """
        constraint_sql = ""
        order = "desc"
        if max_id:
            constraint_sql = "AND tagged_file.sharedfile_id < %s" % (
                int(max_id))
        elif since_id:
            order = "asc"
            constraint_sql = "AND tagged_file.sharedfile_id > %s" % (
                int(since_id))

        sql = """SELECT sharedfile.* FROM sharedfile, tagged_file
                 WHERE tagged_file.tag_id = %s 
                    AND tagged_file.sharedfile_id = sharedfile.id 
                    AND tagged_file.deleted = 0
                 %s
                 ORDER BY tagged_file.sharedfile_id %s limit %s, %s""" % (int(
            self.id), constraint_sql, order, 0, int(per_page))
        results = sharedfile.Sharedfile.object_query(sql)

        if order == "asc":
            results.reverse()

        return results
Ejemplo n.º 11
0
class TaggedFile(ModelQueryCache, Model):
    sharedfile_id = Property()
    tag_id = Property()
    deleted = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(TaggedFile, self).save(*args, **kwargs)

    def _set_dates(self):
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
Ejemplo n.º 12
0
class NSFWLog(Model):
    user_id = Property()
    sharedfile_id = Property()
    sourcefile_id = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(NSFWLog, self).save(*args, **kwargs)

    def _set_dates(self):
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow()
Ejemplo n.º 13
0
class ShakeCategory(Model):
    name = Property(default='')
    short_name = Property(default='')
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(ShakeCategory, self).save(*args, **kwargs)

    def _set_dates(self):
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow()
        self.updated_at = datetime.utcnow()
Ejemplo n.º 14
0
class Apihit(Model):
    accesstoken_id = Property()
    hits = Property()
    hour_start = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Apihit, self).save(*args, **kwargs)

    def _set_dates(self):
        if self.id is None or self.hour_start is None:
            self.hour_start = datetime.utcnow().strftime("%Y-%m-%d %H:00:00")

    @classmethod
    def hit(cls, accesstoken_id, utcnow=None):
        if utcnow is None:
            utcnow = datetime.utcnow()
        hour_start = utcnow.strftime('%Y-%m-%d %H:00:00')

        sql = """SET @total_hits := 1;
                 INSERT INTO apihit (accesstoken_id, hits, hour_start) VALUES (%s, 1, %s)
                 ON DUPLICATE KEY
                    UPDATE hits = (@total_hits := (hits + 1));
                 SELECT @total_hits AS hits;"""
        args = (accesstoken_id, hour_start)
        kwargs = ()

        conn = connection()
        cursor = conn._cursor()
        try:
            conn._execute(cursor, sql, args, kwargs)
            # The SELECT was in the third statement, so the value is the third result set.
            cursor.nextset()
            cursor.nextset()
            (hits, ) = cursor.fetchone()
        finally:
            cursor.close()

        return hits
Ejemplo n.º 15
0
class MigrationState(Model):
    user_id = Property()
    is_migrated = Property()

    @staticmethod
    def has_migration_data(user_id):
        state = MigrationState.get("user_id=%s", user_id)
        if state is None:
            # no state row means this user has nothing to migrate
            return False
        return True

    @staticmethod
    def has_migrated(user_id):
        state = MigrationState.get("user_id=%s", user_id)
        if state is None:
            # no state row means this user has nothing to migrate
            return True

        return state.is_migrated == 1
Ejemplo n.º 16
0
class Apilog(Model):
    accesstoken_id = Property()
    nonce = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Apilog, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
Ejemplo n.º 17
0
class Accesstoken(Model):
    user_id = Property(name='user_id')
    app_id = Property(name='app_id')
    consumer_key = Property(name='consumer_key')
    consumer_secret = Property(name='consumer_secret')
    deleted = Property(name='deleted', default=0)
    created_at = Property(name='created_at')
    updated_at = Property(name='updated_at')

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Accesstoken, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def delete(self):
        """
        Sets deleted flag to true and saves.
        """
        self.deleted = True
        return self.save()

    @staticmethod
    def generate(authorization_id):
        """
        Generate an access token based on an (unexpired) authorization id.
        """
        auth = authorizationcode.Authorizationcode.get('id=%s',
                                                       authorization_id)
        consumer_key = uuid.uuid3(
            uuid.NAMESPACE_DNS,
            base36encode(auth.id) + '-' + base36encode(auth.app_id))
        consumer_secret = sha224("%s%s" %
                                 (str(uuid.uuid1()), time.time())).hexdigest()

        if auth.expires_at > datetime.utcnow():
            access_token = Accesstoken(user_id=auth.user_id,
                                       app_id=auth.app_id,
                                       consumer_key=str(consumer_key),
                                       consumer_secret=str(consumer_secret))
            access_token.save()
            return access_token
        else:
            return None
Ejemplo n.º 18
0
class Waitlist(Model):
    email = Property()
    verification_key = Property()
    verified = Property()
    invited = Property()
    created_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Waitlist, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
Ejemplo n.º 19
0
class ShakeManager(Model):
    shake_id = Property()
    user_id = Property()
    deleted = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        if not self._validate_shake_and_user():
            return False
        return super(ShakeManager, self).save(*args, **kwargs)

    def _validate_shake_and_user(self):
        if int(self.shake_id) <= 0:
            self.add_error('shake', "No shake specified")
            return False
        if int(self.user_id) <= 0:
            self.add_error('user', "No user specified")
            return False
        return True

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def delete(self):
        self.deleted = 1
        self.save()
Ejemplo n.º 20
0
class Authorizationcode(Model):
    user_id = Property()
    app_id = Property()
    code = Property()
    expires_at = Property()
    redeemed = Property(default=0)
    redirect_url = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Authorizationcode, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    @staticmethod
    def generate(app_id, redirect_url, user_id):
        """
        Generate a code based on the app_id, time, and redirect_url
        Set expires_at to be 30 seconds from now.
        """
        code = generate_digest_from_dictionary([
            app_id,
            random.random(),
            time.mktime(datetime.utcnow().timetuple())
        ])
        expires_at = datetime.utcnow() + timedelta(seconds=30)
        auth_code = Authorizationcode(
            user_id=user_id,
            app_id=app_id,
            code=code,
            redirect_url=redirect_url,
            expires_at=expires_at.strftime("%Y-%m-%d %H:%M:%S"))
        auth_code.save()
        return auth_code
Ejemplo n.º 21
0
class User(ModelQueryCache, Model):
    name = Property()
    email = Property()
    hashed_password = Property()
    email_confirmed = Property(default=0)
    full_name = Property(default='')
    about = Property(default='')
    website = Property(default='')
    nsfw = Property(default=0)
    recommended = Property(default=0)
    is_paid = Property(default=0)
    deleted = Property(default=0)
    restricted = Property(default=0)
    tou_agreed = Property(default=0)
    show_naked_people = Property(default=0)
    show_stats = Property(default=0)
    disable_autoplay = Property(default=0)
    verify_email_token = Property()
    reset_password_token = Property()
    profile_image = Property()
    invitation_count = Property(default=0)
    disable_notifications = Property(default=0)
    stripe_customer_id = Property()
    stripe_plan_id = Property()
    stripe_plan_rate = Property()
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        if not self._validate_name():
            return False
        if not self._validate_email():
            return False
        if not self._validate_full_name():
            return False
        if not self._validate_about():
            return False
        if not self._validate_website():
            return False
        if not self.saved():
            # only run email and name validations on create for now
            if not self._validate_name_uniqueness():
                return False
            if not self._validate_email_uniqueness():
                return False

        # Since we add errors / validate outside the save method
        # see set_and_confirm password.
        if len(self.errors) > 0:
            return False
        self._set_dates()
        return super(User, self).save(*args, **kwargs)

    def on_create(self):
        new_shake = shake.Shake(user_id=self.id,
                                type='user',
                                description='New Shake')
        new_shake.save()

    def as_json(self, extended=False):
        base_dict = {
            'name': self.name,
            'id': self.id,
            'profile_image_url': self.profile_image_url(include_protocol=True)
        }

        if extended:
            base_dict['about'] = self.about
            base_dict['website'] = self.website
            shakes = self.shakes()
            base_dict['shakes'] = []
            for shake in shakes:
                base_dict['shakes'].append(shake.as_json())

        return base_dict

    def set_and_confirm_password(self, password, confirm_password):
        if password == None or password == "":
            self.add_error('password', "Passwords can't be blank.")
            return False
        if password != confirm_password:
            self.add_error('password', "Passwords don't match.")
            return False

        if password in bad_list:
            self.add_error(
                'password',
                "That is not a good password. <a target=\"_blank\" href=\"/faq/#bad_password\">Why?</a>"
            )
            return False

        self.set_password(password)
        return True

    def is_plus(self):
        return self.stripe_plan_id == "mltshp-double"

    def is_member(self):
        return self.is_paid

    def set_password(self, password):
        """
        Sets the hashed_password correctly.
        """
        self.hashed_password = self.generate_password_digest(password)

    def add_invitations(self, count=0):
        """
        Adds invitations to a user's account
        """
        self.invitation_count = int(count)
        self.save()

    def send_invitation(self, email_address):
        if self.invitation_count > 0:
            if (email_address != None) and (
                    email_address != '') and email_re.match(email_address):
                if invitation.Invitation.create_for_email(
                        email_address, self.id):
                    self.invitation_count -= 1
                    self.save()
                    return True
        return False

    def invalidate_email(self):
        self.email_confirmed = 0
        h = hashlib.sha1()
        h.update("%s" % (time.time()))
        h.update("%s" % (random.random()))
        self.verify_email_token = h.hexdigest()
        self.save()
        if not options.debug:
            pm = postmark.PMMail(
                api_key=options.postmark_api_key,
                sender="*****@*****.**",
                to=self.email,
                subject="[mltshp] Please verify your email address",
                text_body=
                "Hi there, could you visit this URL to verify your email address for us? Thanks. \n\nhttps://%s/verify-email/%s"
                % (options.app_host, self.verify_email_token))
            pm.send()
            return True
        return False

    def create_reset_password_token(self):
        """
        This function will set the reset token and email the user
        """

        h = hashlib.sha1()
        h.update("%s" % (time.time()))
        h.update("%s" % (random.random()))
        self.reset_password_token = h.hexdigest()
        self.save()
        body = """
Hi there,

We just received a password reset request for this email address (user: %s). If you want to change your password just click this link:
https://%s/account/reset-password/%s

Thanks for using the site!
[email protected]

(If you're having problems with your account, please mail us! We are happy to help.)
""" % (self.name, options.app_host, self.reset_password_token)
        if not options.debug:
            pm = postmark.PMMail(api_key=options.postmark_api_key,
                                 sender="*****@*****.**",
                                 to=self.email,
                                 subject="[mltshp] Password change request",
                                 text_body=body)
            pm.send()

    def sharedimages(self):
        """
        This is a bit of a hack, but I wanted it in the model so I could fix it up later.
        This simply pulls all the shared images and adds one new field,
        which is a signed url to the amazon s3 thumbnail rather than through the server.
        Does not include deleted images.
        """
        images = self.connection.query(
            "SELECT sf.id, sf.title, sf.name, sf.share_key, src.file_key, \
            src.thumb_key FROM sharedfile as sf, sourcefile as src \
            WHERE src.id = sf.source_id AND sf.user_id = %s and sf.deleted = 0 ORDER BY sf.created_at DESC limit 3",
            self.id)
        for image in images:
            file_path = "thumbnails/%s" % (image['thumb_key'])
            authenticated_url = s3_authenticated_url(options.aws_key, options.aws_secret, \
                bucket_name=options.aws_bucket, file_path=file_path)
            image['thumbnail_url'] = authenticated_url
        return images

    def set_profile_image(self, file_path, file_name, content_type):
        """
        Takes a local path, name and content-type, which are parameters passed in by
        nginx upload module.  Converts to RGB, resizes to thumbnail and uploads to S3.
        Returns False if some conditions aren't met, such as error making thumbnail
        or content type is one we don't support.
        """
        valid_content_types = (
            'image/gif',
            'image/jpeg',
            'image/jpg',
            'image/png',
        )
        if content_type not in valid_content_types:
            return False

        destination = cStringIO.StringIO()
        if not transform_to_square_thumbnail(file_path, 100 * 2, destination):
            return False

        bucket = S3Bucket()
        k = Key(bucket)
        k.key = "account/%s/profile.jpg" % (self.id)
        k.set_metadata('Content-Type', 'image/jpeg')
        k.set_metadata('Cache-Control', 'max-age=86400')
        k.set_contents_from_string(destination.getvalue())
        k.set_acl('public-read')
        self.profile_image = 1
        self.save()
        return True

    def profile_image_url(self, include_protocol=False):
        protocol = ''
        if self.profile_image:
            if include_protocol:
                if options.app_host == 'mltshp.com':
                    protocol = 'https:'
                else:
                    protocol = 'http:'
            if options.app_host == 'mltshp.com':
                aws_url = "%s//%s.%s" % (protocol, options.aws_bucket,
                                         options.aws_host)
            else:
                # must be running for development. use the /s3 alias
                aws_url = "/s3"
            return "%s/account/%s/profile.jpg" % (aws_url, self.id)
        else:
            if include_protocol:
                if options.app_host == 'mltshp.com':
                    return "https://%s/static/images/default-icon-venti.svg" % options.cdn_ssl_host
                elif options.use_cdn and options.cdn_host:
                    return "http://%s/static/images/default-icon-venti.svg" % options.cdn_host
            return "/static/images/default-icon-venti.svg"

    def sharedfiles(self, page=1, per_page=10):
        """
        Shared files, paginated.
        """
        limit_start = (page - 1) * per_page
        #return Sharedfile.where("user_id = %s and deleted=0 order by id desc limit %s, %s ", self.id, int(limit_start), per_page)
        user_shake = self.shake()
        return user_shake.sharedfiles(page=page, per_page=per_page)

    def sharedfiles_count(self):
        """
        Count of all of a user's saved sharedfiles, excluding deleted.
        """
        #return Sharedfile.where_count("user_id = %s and deleted=0", self.id)
        user_shake = self.shake()
        return user_shake.sharedfiles_count()

    def likes(self, before_id=None, after_id=None, per_page=10):
        """
        User's likes, paginated.
        """
        return Sharedfile.favorites_for_user(self.id,
                                             before_id=before_id,
                                             after_id=after_id,
                                             per_page=per_page)

    def likes_count(self):
        """
        Count of all of a user's saved sharedfiles, excluding deleted.
        """
        return models.favorite.Favorite.where_count(
            "user_id = %s and deleted=0", self.id)

    def sharedfiles_from_subscriptions(self,
                                       before_id=None,
                                       after_id=None,
                                       per_page=10):
        """
        Shared files from subscriptions, paginated.
        """
        return Sharedfile.from_subscriptions(user_id=self.id,
                                             before_id=before_id,
                                             after_id=after_id,
                                             per_page=per_page)

    def has_favorite(self, sharedfile):
        """
        Returns True if user has already favorited the sharedfile, False otherwise.
        """
        if models.favorite.Favorite.get(
                'user_id = %s and sharedfile_id = %s and deleted = 0' %
            (self.id, sharedfile.id)):
            return True
        else:
            return False

    def saved_sharedfile(self, sharedfile):
        """
        Return sharedfile if they have saved the file, otherwise None.

        We limit the get query to 1, because theroetically a user could have saved
        the same files multiple times, since we never enforced it, and .get throws
        exception when more than one returned.
        """
        saved = Sharedfile.get(
            'user_id = %s and parent_id=%s and deleted = 0 limit 1' %
            (self.id, sharedfile.id))
        if saved:
            return saved
        else:
            return None

    def add_favorite(self, sharedfile):
        """
        Add a sharedfile as a favorite for the user.

        Will return False when one can't favorite a shared file:
         - it's deleted
         - it belongs to current user
         - already favorited

        Will return True if favoriting succeeds.
        """
        if sharedfile.deleted:
            return False
        if sharedfile.user_id == self.id:
            return False

        existing_favorite = models.favorite.Favorite.get(
            'user_id = %s and sharedfile_id=%s' % (self.id, sharedfile.id))
        if existing_favorite:
            if existing_favorite.deleted == 0:
                return False
            existing_favorite.deleted = 0
            existing_favorite.save()
        else:
            favorite = models.favorite.Favorite(user_id=self.id,
                                                sharedfile_id=sharedfile.id)
            try:
                favorite.save()
            # This can only happen in a race condition, when request gets
            # sent twice (like during double click).  We just assume it worked
            # the first time and return a True.
            except IntegrityError:
                return True
            notification.Notification.new_favorite(self, sharedfile)

        calculate_likes.delay_or_run(sharedfile.id)
        return True

    def remove_favorite(self, sharedfile):
        """
        Remove a favorite. If there is no favorite or if it's already been remove, return False.
        """
        existing_favorite = models.favorite.Favorite.get(
            'user_id= %s and sharedfile_id = %s and deleted=0' %
            (self.id, sharedfile.id))
        if not existing_favorite:
            return False
        if existing_favorite.deleted:
            return False
        existing_favorite.deleted = 1
        existing_favorite.save()

        calculate_likes.delay_or_run(sharedfile.id)
        return True

    def subscribe(self, to_shake):
        """
        Subscribe to a shake. If subscription already exists, then just mark deleted as 0.
        If this is a new subscription, send notification email.
        """
        if to_shake.deleted != 0:
            return False

        if to_shake.user_id == self.id:
            #you can't subscribe to your own shake, dummy!
            return False

        existing_subscription = subscription.Subscription.get(
            'user_id = %s and shake_id = %s', self.id, to_shake.id)
        if existing_subscription:
            existing_subscription.deleted = 0
            existing_subscription.save()
        else:
            try:
                new_subscription = subscription.Subscription(
                    user_id=self.id, shake_id=to_shake.id)
                new_subscription.save()
                notification.Notification.new_subscriber(
                    sender=self,
                    receiver=to_shake.owner(),
                    action_id=new_subscription.id)
            # if we get an integrity error, means we already subscribed successfully, so carry along.
            except IntegrityError:
                pass
        return True

    def unsubscribe(self, from_shake):
        """
        Mark a subscription as deleted. If it doesn't exist, just return
        """
        if from_shake.user_id == self.id:
            #you can't unsubscribe to your own shake, dummy!
            return False

        existing_subscription = subscription.Subscription.get(
            'user_id = %s and shake_id = %s', self.id, from_shake.id)
        if existing_subscription:
            existing_subscription.deleted = 1
            existing_subscription.save()
        return True

    def subscribe_to_user(self, shake_owner):
        """
        When a user hits a follow button by a user's name, will follow the users' main shake.
        """
        if self.id == shake_owner.id:
            return False

        shake_owners_shake = shake.Shake.get(
            'user_id = %s and type=%s and deleted=0', shake_owner.id, 'user')
        return self.subscribe(shake_owners_shake)

    def total_file_stats(self):
        """
        Returns the file like, save, and view counts
        """
        counts = sharedfile.Sharedfile.query(
            "SELECT sum(like_count) as likes, sum(save_count) as saves, sum(view_count) as views from sharedfile where user_id = %s AND deleted=0",
            self.id)
        counts = counts[0]
        for key, value in counts.items():
            if not value:
                counts[key] = 0
        return counts

    def unsubscribe_from_user(self, shake_owner):
        """
        When a user hits the unfollow button by a user's name, will  unfollow the users' main shake.
        """
        if self.id == shake_owner.id:
            return False

        shake_owners_shake = shake.Shake.get(
            'user_id = %s and type=%s and deleted=0', shake_owner.id, 'user')
        return self.unsubscribe(shake_owners_shake)

    def has_subscription(self, user):
        """
        Returns True if a user subscribes to user's main shake
        """
        users_shake = shake.Shake.get(
            'user_id = %s and type = %s and deleted=0', user.id, 'user')
        return self.has_subscription_to_shake(users_shake)

    def has_subscription_to_shake(self, shake):
        """
        This should replace the above method completely.
        """
        if not shake:
            return False

        existing_subscription = subscription.Subscription.get(
            'user_id = %s and shake_id = %s and deleted = 0', self.id,
            shake.id)
        if existing_subscription:
            return True
        else:
            return False

    def can_create_shake(self):
        """
        To create a shake a user needs to be paid and can only create
        ten shakes.
        """
        if options.readonly:
            return False
        if not self.is_plus():
            return False
        if len(self.shakes(include_only_group_shakes=True)) <= 100:
            return True
        return False

    def create_group_shake(self, title=None, name=None, description=None):
        """THERE IS NO (except for name) ERROR CHECKING HERE"""
        new_shake = None
        if not name:
            return None

        current_shakes = self.shakes()

        if len(current_shakes) == 1 or self.is_admin():
            new_shake = shake.Shake(user_id=self.id,
                                    type='group',
                                    title=title,
                                    name=name,
                                    description=description)
            new_shake.save()

        return new_shake

    def delete(self):
        if options.readonly:
            return False

        self.deleted = 1
        self.email = '*****@*****.**' % (self.id)
        self.hashed_password = '******'
        self.nsfw = 1
        self.verify_email_token = 'deleted'
        self.reset_password_token = 'deleted'
        self.profile_image = 0
        self.disable_notifications = 1
        self.invitation_count = 0

        if self.stripe_customer_id:
            # cancel any existing subscription
            customer = None
            try:
                customer = stripe.Customer.retrieve(self.stripe_customer_id)
            except stripe.error.InvalidRequestError:
                pass
            if customer and not getattr(customer, 'deleted', False):
                # deleting the customer object will also delete
                # active subscriptions
                customer.delete()

        self.save()

        external_services = externalservice.Externalservice.where(
            "user_id=%s and deleted=0", self.id)
        for service in external_services:
            service.deleted = 1
            service.save()

        user_shake = self.shake()
        subscriptions = subscription.Subscription.where(
            "user_id=%s or shake_id=%s", self.id, user_shake.id)
        for sub in subscriptions:
            sub.deleted = 1
            sub.save()

        shakemanagers = shakemanager.ShakeManager.where(
            "user_id=%s and deleted=0", self.id)
        for sm in shakemanagers:
            sm.delete()

        shakes = shake.Shake.where("user_id = %s and deleted=0", self.id)
        for s in shakes:
            s.deleted = 1
            s.save()

        comments = models.comment.Comment.where('user_id=%s and deleted=0',
                                                self.id)
        for com in comments:
            com.deleted = 1
            com.save()

        favorites = models.favorite.Favorite.where('user_id=%s and deleted=0',
                                                   self.id)
        for fav in favorites:
            fav.deleted = 1
            fav.save()

        notifications = models.notification.Notification.where(
            'sender_id=%s and deleted=0', self.id)
        for no in notifications:
            no.deleted = 1
            no.save()

        shared_files = sharedfile.Sharedfile.where('user_id=%s and deleted=0',
                                                   self.id)
        for sf in shared_files:
            sf.delete()

        return True

    def shake(self):
        return shake.Shake.get('user_id=%s and type=%s and deleted=0', self.id,
                               'user')

    def shakes(self, include_managed=False, include_only_group_shakes=False):
        """
        Returns all the shakes this user owns. That is, has a user_id = self.id
        in the shake table. If include_managed is True, also return those shakes
        this user manages in the manage table.  If include_only_group_shakes is True
        it will not return any 'user' shakes that the user owns.
        """
        managed_shakes = []
        if include_managed:
            sql = """SELECT shake.* from shake, shake_manager
                        WHERE shake_manager.user_id = %s
                            AND shake.id = shake_manager.shake_id
                            AND shake.deleted = 0
                            AND shake_manager.deleted = 0
                        ORDER BY shake_manager.shake_id
            """
            managed_shakes = shake.Shake.object_query(sql, self.id)
        user_shakes_sql = 'user_id=%s ORDER BY id'
        if include_only_group_shakes:
            user_shakes_sql = "user_id=%s and type='group' and deleted=0 ORDER BY id"
        return shake.Shake.where(user_shakes_sql, self.id) + managed_shakes

    _has_multiple_shakes = None

    def has_multiple_shakes(self):
        """
        A method we call when we render uimodule.Image to determine if user
        can save to more than one shake, thus giving them a different "save this"
        interaction.  We cache the results to cut down on the DB queries.
        """
        if self._has_multiple_shakes is None:
            if len(self.shakes()) > 1:
                self._has_multiple_shakes = True
            else:
                self._has_multiple_shakes = False
        return self._has_multiple_shakes

    def following_count(self):
        sql = """
          SELECT count(user.id) as following_count
            FROM subscription, user, shake
              WHERE subscription.user_id = %s
              AND subscription.shake_id = shake.id
              AND user.id = shake.user_id
              AND shake.deleted = 0
              AND subscription.deleted = 0
        """ % self.id
        count = self.query(sql)
        return int(count[0]['following_count'])

    def following(self, page=None):
        """
        This needs to be refactored, but it would be so slow if we
        were grabbing user objects for each user on 1,000 users.
        """
        select = """
          SELECT user.id as user_id, user.name as user_name, user.profile_image as user_image,
                    shake.name as shake_name, shake.type as shake_type , shake.image as shake_image,
                    shake.id as shake_id
            FROM subscription, user, shake
              WHERE subscription.user_id = %s
              AND subscription.shake_id = shake.id
              AND user.id = shake.user_id
              AND shake.deleted = 0
              AND subscription.deleted = 0
        """ % self.id

        if page > 0:
            limit_start = (page - 1) * 20
            select = "%s LIMIT %s, %s" % (select, limit_start, 20)

        users_and_shakes = User.query(select)

        us_list = []
        for us in users_and_shakes:
            this_follow = {}
            this_follow['image'] = '/static/images/default-icon-venti.svg'
            if us['shake_type'] == 'user':
                this_follow['id'] = us['user_id']
                this_follow['path'] = '/user/%s' % (us['user_name'])
                this_follow['name'] = us['user_name']
                this_follow['type'] = 'user'
                if us['user_image']:
                    this_follow['image'] = s3_url("account/%s/profile.jpg" %
                                                  us['user_id'])
            else:
                this_follow['id'] = us['shake_id']
                this_follow['path'] = '/%s' % (us['shake_name'])
                this_follow['name'] = us['shake_name']
                this_follow['type'] = 'shake'
                if us['shake_image']:
                    this_follow['image'] = s3_url(
                        "account/%s/shake_%s.jpg" %
                        (us['user_id'], us['shake_name']))

            us_list.append(this_follow)
        return us_list

    def can_follow(self, shake):
        """
        A user can follow a shake only if it doesn't belong
        to them.
        """
        if options.readonly:
            return False
        if not self.is_paid:
            return False
        if shake.deleted != 0:
            return False
        if shake.user_id == self.id:
            return False
        return True

    def display_name(self):
        if self.full_name:
            return self.full_name
        else:
            return self.name

    def flag_nsfw(self):
        """
        Set the nsfw flag for user.
        """
        self.nsfw = True
        return self.save()

    def unflag_nsfw(self):
        """
        Remove the nsfw flag for user.
        """
        self.nsfw = False
        return self.save()

    def is_superuser(self):
        """
        Return True if user is a superuser administrator.
        """
        return self.name in options.superuser_list.split(',')

    def is_moderator(self):
        """
        Return True if user is a moderator.
        """
        return self.name in options.moderator_list.split(',')

    def is_admin(self):
        """
        Return True if user is a superuser or moderator.
        """
        return self.is_superuser() or self.is_moderator()

    def update_email(self, email):
        """
        Since updating an email is a bit tricky, call this method instead of
        assigning to the property directly.  Validates the name and email
        address and will assign errors if its already taken, which will
        prevent saving.
        """
        email = email.strip().lower()
        if email != self.email:
            self.email = email
            self._validate_email_uniqueness()

    def uploaded_kilobytes(self, start_time=None, end_time=None):
        """
        Returns the total number of kilobytes uploaded for the time period specified
        If no time period specified, returns total of all time.
        """

        start_string = ''
        end_string = ''
        if start_time:
            start_string = " AND created_at >= '%s 00:00:00'" % (start_time)

        if end_time:
            end_string = " AND created_at <= '%s 23:59:59'" % (end_time)

        sql = "SELECT SUM(size) as total_bytes FROM sharedfile WHERE user_id = %s " + start_string + end_string
        response = self.query(sql, self.id)

        if not response[0]['total_bytes'] or int(
                response[0]['total_bytes']) == 0:
            return 0
        else:
            return int(response[0]['total_bytes'] / 1024)

    def can_post(self):
        return self.can_upload_this_month()

    def can_upload_this_month(self):
        """
        Returns if this user can upload this month.
        If is_paid or under the max_mb_per_month setting: True
        """
        if not self.is_paid:
            return False

        if self.is_plus():
            return True

        month_days = calendar.monthrange(datetime.utcnow().year,
                                         datetime.utcnow().month)
        start_time = datetime.utcnow().strftime("%Y-%m-01")
        end_time = datetime.utcnow().strftime("%Y-%m-" + str(month_days[1]))

        total_bytes = self.uploaded_kilobytes(start_time=start_time,
                                              end_time=end_time)

        if total_bytes == 0:
            return True

        total_megs = total_bytes / 1024

        if total_megs > options.max_mb_per_month:
            return False
        else:
            return True

    def can_request_invitation_to_shake(self, shake_id):
        if options.readonly:
            return False

        #shake exists
        s = shake.Shake.get('id = %s and deleted=0', shake_id)
        if not s:
            return False

        #not a manager of the shake
        for manager in s.managers():
            if manager.id == self.id:
                return False

        #is not owner of the shake
        if self.id == s.user_id:
            return False

        #no notification exists
        no = notification.Notification.get(
            'sender_id = %s and receiver_id = %s and action_id = %s and type = %s and deleted = 0',
            self.id, s.user_id, s.id, 'invitation_request')
        if no:
            return False

        return True

    def request_invitation_to_shake(self, shake_id):
        s = shake.Shake.get('id=%s and deleted=0', shake_id)
        if s:
            manager = s.owner()
            no = notification.Notification.new_invitation_to_shake(
                self, manager, s.id)

    def active_paid_subscription(self):
        if self.stripe_customer_id is not None:
            # fetch customer then find active plan
            customer = None
            try:
                customer = stripe.Customer.retrieve(self.stripe_customer_id)
            except stripe.error.InvalidRequestError:
                pass
            if customer and not getattr(customer, 'deleted', False):
                subs = []
                if customer.subscriptions.total_count > 0:
                    subs = [
                        s for s in customer.subscriptions.data
                        if s.status == "active" and s.plan.id in (
                            "mltshp-single", "mltshp-double")
                    ]
                if subs:
                    return {
                        "processor_name":
                        "Stripe",
                        "id":
                        subs[0].id,
                        "start_date":
                        datetime.fromtimestamp(
                            subs[0].current_period_start).strftime(
                                "%Y-%m-%d %H:%M:%S"),
                        "end_date":
                        datetime.fromtimestamp(
                            subs[0].current_period_end).strftime(
                                "%Y-%m-%d %H:%M:%S"),
                    }
        else:
            processors = {
                PaymentLog.VOUCHER: {
                    "name": "Voucher",
                },
                PaymentLog.STRIPE: {
                    "name": "Stripe",
                },
            }
            pl = PaymentLog.last_payments(1, user_id=self.id)
            if pl and pl[0].processor in processors:
                return {
                    "processor_name": processors[pl[0].processor]["name"],
                    "start_date": pl[0].transaction_date,
                    "end_date": pl[0].next_transaction_date,
                    "id": pl[0].subscription_id,
                }
        return None

    def _validate_name_uniqueness(self):
        """
        Validation only run on creation, for now, also needs to
        run if value has changed
        """
        if self.get("name = %s", self.name):
            self.add_error('name', 'Username has already been taken.')
            return False
        return True

    def _validate_email_uniqueness(self):
        """
        Validation only run on creation, for now, also needs to
        run if value has changed
        """
        email = self.email.strip().lower()
        if self.get("email = %s", email):
            self.add_error('email', 'This email already has an account.')
            return False
        return True

    def _validate_name(self):
        """
        At some point, this and the below validations belong in the model.
        """
        if self.name == None or self.name == "":
            self.add_error('name', 'You definitely need a username')
            return False

        if len(self.name) > 30:
            self.add_error('name',
                           'Username should be less than 30 characters.')
            return False

        if re.search("[^a-zA-Z0-9\-\_]", self.name):
            self.add_error(
                'name',
                'Username can only contain letters, numbers, dash and underscore characters.'
            )
            return False
        return True

    def _validate_email(self):
        """
        Borrowed from Django's email validation.
        """
        if self.email == None or self.email == "":
            self.add_error('email',
                           "You'll need an email to verify your account.")
            return False

        if not email_re.match(self.email):
            self.add_error('email', "Email doesn't look right.")
        return True

    def _validate_full_name(self):
        if len(self.full_name) > 100:
            self.add_error('full_name', "Name is too long for us.")
            return False
        return True

    def _validate_about(self):
        if len(self.about) > 255:
            self.add_error(
                'about',
                "The about text needs to be shorter than 255 characters.")
            return False
        return True

    def _validate_website(self):
        if len(self.website) > 255:
            self.add_error('website', "The URL is too long.")
            return False
        if self.website != '':
            parsed = urlparse.urlparse(self.website)
            if parsed.scheme not in (
                    'http',
                    'https',
            ):
                self.add_error('website', "Doesn't look to be a valid URL.")
                return False
            if parsed.netloc == '':
                self.add_error('website', "Doesn't look to be a valid URL.")
                return False
        return True

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow()
        self.updated_at = datetime.utcnow()

    @classmethod
    def random_recommended(self, limit):
        """
        Return a randomized list of users that have recommended flag set.
        """
        return self.where(
            "recommended = 1 and deleted = 0 order by rand() limit %s", limit)

    @classmethod
    def recommended_for_user(self, user):
        """
        Returns a list of users that the passed in user doesn't follow, but
        maybe should.
        """
        following_sql = """
            select user.id from user
                left join shake
                on shake.user_id = user.id and shake.deleted=0
                left join subscription
                on subscription.shake_id = shake.id
                where user.deleted = 0 and subscription.user_id  = %s and subscription.deleted=0
        """
        following = self.query(following_sql, user.id)
        following = [somebody['id'] for somebody in following]

        all_users_sql = """
            select id from user where deleted=0
        """
        all_users = self.query(all_users_sql)
        all_users = [somebody['id'] for somebody in all_users]

        not_following = set(all_users) - set(following)

        users_that_favorited_sql = """
            select s1.user_id as s1_user, s2.user_id as s2_user from sharedfile as s1
            left join sharedfile s2
            on s1.parent_id = s2.id
            left join favorite
            on favorite.sharedfile_id = s1.id
            where favorite.deleted = 0
            and favorite.user_id = %s
            limit 1000
        """
        users_that_favorited_result = self.query(users_that_favorited_sql,
                                                 user.id)
        users_that_favorited = [somebody['s1_user'] for somebody in users_that_favorited_result] + \
                               [somebody['s2_user'] for somebody in users_that_favorited_result if somebody]

        not_following_favorited = not_following & set(users_that_favorited)

        if len(not_following_favorited) == 0:
            return []

        if len(not_following_favorited) < 5:
            sample_size = len(not_following_favorited)
        else:
            sample_size = 5

        samples = random.sample(not_following_favorited, sample_size)
        users = []
        for user_id in samples:
            fetched_user = User.get("id = %s and deleted = 0", user_id)
            if fetched_user:
                users.append(fetched_user)

        return users

    @classmethod
    def find_by_name_fragment(self, name=None, limit=10):
        """
        Finds the user by using a fragment of their name. Name must start
        with the fragment.
        """
        if name == '' or name == None:
            return []
        name = name + '%'
        return User.where("name like %s and deleted=0 limit %s", name, limit)

    @staticmethod
    def authenticate(name, password):
        """
        Returns User object or None.
        """
        hashed_password = User.generate_password_digest(password)
        return User.get("name = %s and hashed_password = %s and deleted = 0",
                        name, hashed_password)

    @staticmethod
    def find_unmigrated_user(name, password):
        """
        Returns a non-migrated User object or None.

        This code is part of the MLKSHK->MLTSHP migration project. It can be removed
        once the migration is over. We are tracking unmigrated users as deleted records,
        with a deleted value of 2.
        """
        hashed_password = User.generate_password_digest(password)
        return User.get("name = %s and hashed_password = %s and deleted = 2",
                        name, hashed_password)

    @staticmethod
    def generate_password_digest(password):
        secret = options.auth_secret
        h = hashlib.sha1()
        h.update(password)
        h.update(secret)
        return h.hexdigest()
Ejemplo n.º 22
0
class Sourcefile(ModelQueryCache, Model):
    width = Property()
    height = Property()
    data = Property()
    type = Property()
    file_key = Property()
    thumb_key = Property()
    small_key = Property()
    nsfw = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        """
        Sets the dates before saving.
        """
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        super(Sourcefile, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def width_constrained_dimensions(self, width_constraint):
        """
        This is used to figure out the size at which something will render on
        a page, which is constrained with a width of 550px. For photos, the
        image size will be scaled down if it's too big, and otherwise, it will
        be presented as is. Videos also get scaled down if too big, but they also
        get enlarged to fit the width if smaller than constraint.
        """
        if self.width <= width_constraint and self.type == 'image':
            return (self.width, self.height)
        rescale_ratio = float(width_constraint) / self.width
        return (int(self.width * rescale_ratio),
                int(self.height * rescale_ratio))

    def nsfw_bool(self):
        """
        NSFW flag cast to a boolean.
        """
        if not self.nsfw or self.nsfw == 0:
            return False
        return True

    @staticmethod
    def get_by_file_key(file_key):
        """
        Returns a Sourcefile by its file_key.
        """
        return Sourcefile.get("file_key = %s", file_key)

    @staticmethod
    def get_sha1_url_key(url=None):
        if not url:
            return None
        h = hashlib.sha1()
        h.update(url)
        return h.hexdigest()

    @staticmethod
    def get_sha1_file_key(file_path=None, file_data=None):
        if not file_data:
            try:
                fh = open(file_path, 'r')
                file_data = fh.read()
                fh.close()
            except Exception as e:
                return None

        h = hashlib.sha1()
        h.update(file_data)
        return h.hexdigest()

    @staticmethod
    def get_from_file(file_path, sha1_value, type='image', skip_s3=None):
        existing_source_file = Sourcefile.get("file_key = %s", sha1_value)
        thumb_cstr = cStringIO.StringIO()
        small_cstr = cStringIO.StringIO()
        if existing_source_file:
            return existing_source_file
        try:
            img = Image.open(file_path)
            original_width = img.size[0]
            original_height = img.size[1]
            image_format = img.format
        except Exception as e:
            return None

        if img.mode != "RGB":
            img = img.convert("RGB")

        #generate smaller versions
        thumb = img.copy()
        small = img.copy()

        thumb.thumbnail((100, 100), Image.ANTIALIAS)
        small.thumbnail((240, 184), Image.ANTIALIAS)

        thumb.save(thumb_cstr, format="JPEG")
        small.save(small_cstr, format="JPEG")

        bucket = None
        if not skip_s3:
            bucket = S3Bucket()

        #save original file
        if type != 'link':
            if not skip_s3:
                k = Key(bucket)
                k.key = "originals/%s" % (sha1_value)
                k.set_contents_from_filename(file_path)

        #save thumbnail
        thumbnail_file_key = Sourcefile.get_sha1_file_key(
            file_data=thumb_cstr.getvalue())
        if not skip_s3:
            k = Key(bucket)
            k.key = "thumbnails/%s" % thumbnail_file_key
            k.set_contents_from_string(thumb_cstr.getvalue())

        #save small
        small_file_key = Sourcefile.get_sha1_file_key(
            file_data=small_cstr.getvalue())
        if not skip_s3:
            k = Key(bucket)
            k.key = "smalls/%s" % small_file_key
            k.set_contents_from_string(small_cstr.getvalue())

        #save source file
        sf = Sourcefile(width=original_width,
                        height=original_height,
                        file_key=sha1_value,
                        thumb_key=thumbnail_file_key,
                        small_key=small_file_key,
                        type=type)
        sf.save()
        return sf

    @staticmethod
    def make_oembed_url(url):
        url_parsed = None
        try:
            url_parsed = urlparse(url)
        except:
            return None

        if url_parsed.hostname.lower() not in [
                'youtube.com', 'www.youtube.com', 'vimeo.com', 'www.vimeo.com',
                'youtu.be', 'flickr.com', 'www.flickr.com', 'vine.co',
                'www.vine.co'
        ]:
            return None

        oembed_url = None
        if url_parsed.hostname.lower() in [
                'youtube.com', 'www.youtube.com', 'youtu.be'
        ]:
            to_url = 'http://%s%s?%s' % (url_parsed.hostname, url_parsed.path,
                                         url_parsed.query)
            oembed_url = 'http://www.youtube.com/oembed?url=%s&maxwidth=550&format=json' % (
                url_escape(to_url))
        elif url_parsed.hostname.lower() in ['vimeo.com', 'www.vimeo.com']:
            to_url = 'http://%s%s' % (url_parsed.hostname, url_parsed.path)
            oembed_url = 'http://vimeo.com/api/oembed.json?url=%s&maxwidth=550' % (
                url_escape(to_url))
        elif url_parsed.hostname.lower() in ['flickr.com', 'www.flickr.com']:
            to_url = 'http://%s%s' % (url_parsed.hostname, url_parsed.path)
            oembed_url = 'http://www.flickr.com/services/oembed/?url=%s&maxwidth=550&format=json' % (
                url_escape(to_url))
        elif url_parsed.hostname.lower() in ['vine.co', 'www.vine.co']:
            clean_path = re.search('^(/v/[a-zA-Z0-9]+)', url_parsed.path)
            if clean_path and clean_path.group(1):
                to_url = 'https://vine.co%s' % (clean_path.group(1))
                iframe = """<iframe class="vine-embed" src="%s/embed/simple" """ \
                    """width="480" height="480" frameborder="0">""" \
                    """</iframe><script async src="//platform.vine.co/static/scripts/embed.js" """ \
                    """charset="utf-8"></script>""" % to_url
                oembed_url = 'data:text/json;charset=utf-8,{"provider_name":"Vine","title":"","html":"%s"}' % iframe.replace(
                    '"', '\\"')
        return oembed_url

    @staticmethod
    def create_from_json_oembed(link=None,
                                oembed_doc=None,
                                thumbnail_file_path=None):
        """
        Ideally this is a link right now. Specificallly a video link.

        JSON object, thumbnail_path, and the actual url comes in, a sha1 should be created from the url and the
            file_key takes that sha1 value. Then call get_from_file with the type=link
            value set along with the thumbnail path in place.

            The resulting sourcefile should then have the data field set with the oembed doc.

            A source file should be created and returned.
        """
        sha1_key = Sourcefile.get_sha1_file_key(file_path=None, file_data=link)
        sf = Sourcefile.get_from_file(thumbnail_file_path,
                                      sha1_key,
                                      type='link')
        if sf:
            sf.data = json_encode(oembed_doc)
            sf.save()
        return sf
Ejemplo n.º 23
0
class Voucher(Model):
    # parent Id of a user, if a user offered the
    # voucher
    offered_by_user_id = Property()

    # Id of user who used the voucher
    claimed_by_user_id = Property()
 
    # somewhat secret key to point to voucher
    voucher_key = Property()
 
    # email address that voucher key was sent to
    # if applicable
    email_address = Property()

    # name of person who voucher was meant for
    name = Property()
        
    created_at = Property()
    claimed_at = Property()

    # promotion this voucher relates to
    promotion_id = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Voucher, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def get_promotion(self):
        if self.promotion_id:
            return promotion.Promotion.get("id = %s", self.promotion_id)
        else:
            return None

    @classmethod
    def create_for_email(self, email, user_id):
        """
        Creates a voucher for an email address.
        """
        h = hashlib.sha1()
        h.update("%s" % (time.time()))
        h.update("%s" % (email))
        voucher_key = h.hexdigest()
        sending_user = user.User.get('id = %s', user_id)
        voucher = Voucher(offered_by_user_id=user_id,
            voucher_key=voucher_key, email_address=email,
            claimed_by_user_id=0)
        voucher.save()
        if not options.debug:
            text_body = """Hi there. A user on MLTSHP named %s has sent you this invitation to join the site.

You can claim it at this URL:

http://mltshp.com/create-account?key=%s

Be sure to check out the incoming page for fresh files being uploaded, when you find someone you want to keep track of, click the "follow" button on their profile to see their files when you first sign in.

We're adding features and making updates daily so please check back often.

Once you have your account set up, check out:
http://mltshp.com/tools/plugins (browser plugins for saving images)
http://mltshp.com/tools/twitter (connecting your phone's Twitter app to use MLTSHP instead of Twitpic or yFrog)
http://twitter.com/mltshp (our twitter account)
http://mltshp.tumblr.com/ (our blog)

- MLTSHP""" % (sending_user.name, voucher_key)
            html_body = """<p>Hi there. A user on MLTSHP named <a href="http://mltshp.com/user/%s">%s</a> has sent you this invitation to join the site.</p>

<p>You can claim it at this URL:</p>

<p><a href="http://mltshp.com/create-account?key=%s">http://mltshp.com/create-account?key=%s</a></p>

<p>Be sure to check out the <a href="http://mltshp.com/incoming">incoming</a> page for fresh files being uploaded, when you find someone you want to keep track of, click the "follow" button on their profile to see their files when you first sign in.</p>

<p>We&#39;re adding features and making updates daily so please check back often.</p>

<p>Once you have your account set up, check out:</p>
<p>
<a href="http://mltshp.com/tools/plugins">http://mltshp.com/tools/plugins</a> (browser plugins for saving images)<br>
<a href="http://mltshp.com/tools/twitter">http://mltshp.com/tools/twitter</a> (connecting your phone's Twitter app to use MLTSHP instead of Twitpic or yFrog)<br>
<a href="http://twitter.com/mltshp">http://twitter.com/mltshp</a> (our twitter account)<br>
<a href="http://mltshp.tumblr.com/">http://mltshp.tumblr.com/</a> (our blog)
</p>
<p>
- MLTSHP
</p>""" % (sending_user.name,sending_user.name, voucher_key, voucher_key)

            pm = postmark.PMMail(api_key=options.postmark_api_key, 
                sender="*****@*****.**", to=email, 
                subject="An Invitation To MLTSHP", 
                text_body=text_body,
                html_body=html_body)
            pm.send()
        return voucher

    @classmethod
    def by_email_address(self, email):
        """
        Returns voucher where email address matches and is not claimed.  We use a 
        where query here since we don't enforce uniqueness on email address.
        Just returns 1st.
        """
        if not email:
            return None
        vouchers = self.where("email_address = %s and claimed_by_user_id = 0", email)
        try:
            return invitations[0]
        except IndexError:
            return None
    
    @classmethod
    def by_voucher_key(self, key):
        """
        Returns voucher by key.  We use a where query here since we don't enforce
        uniqueness on key. Just returns 1st.

        WILL return a voucher even if it is claimed.
        """
        if not key:
            return None
        vouchers = self.where("voucher_key = %s", key)
        try:
            return vouchers[0]
        except IndexError:
            return None
    
    @classmethod
    def by_user(self, user_):
        """
        Returns all vouchers sent out by user.
        """
        return self.where("offered_by_user_id = %s", user_.id)

    def apply_to_user(self, user_):
        from models import PaymentLog

        promotion = self.get_promotion()

        self.offered_by_user_id = 0

        if not self.offered_by_user_id and promotion is not None:
            shake = promotion.shake()
            if shake is not None:
                # if this promotion is related to a shake
                # associate the voucher with that shake's owner
                self.offered_by_user_id = shake.user_id

        now = datetime.datetime.utcnow()

        # if the user has a voucher, then we need
        # to apply a credit to their account using
        # payment_log in addition to creating the
        # voucher record and claiming it.
        self.claimed_by_user_id = user_.id
        self.claimed_at = now.strftime("%Y-%m-%d %H:%M:%S")
        self.save()

        # now record to payment_log that this
        # user has claimed a voucher.
        next_date = None
        amount = None

        promotion = self.get_promotion()
        # make a sensible "transaction amount" description
        # that we use for the settings screen
        if promotion is not None \
            and promotion.membership_months > 0:
            months = promotion.membership_months
            days = int((365 * (months/12.0)) + 0.5)
            next_date = now + datetime.timedelta(days=days)
            if months >= 12 and months % 12 == 0:
                years = months / 12
                if years == 1:
                    amount = '1 Year'
                elif years > 1:
                    amount = '%d Years' % years
            elif months == 1:
                amount = '1 Month'
            elif months > 1:
                amount = '%d Months' % months

            pl = PaymentLog(
                user_id                   = user_.id,
                status                    = "credit",
                reference_id              = promotion.id,
                transaction_id            = self.voucher_key,
                operation                 = "redeem",
                transaction_date          = now.strftime("%Y-%m-%d %H:%M:%S"),
                next_transaction_date     = next_date.strftime("%Y-%m-%d %H:%M:%S"),
                buyer_email               = user_.email,
                buyer_name                = (user_.full_name or user_.name),
                transaction_amount        = amount,
                payment_reason            = "MLTSHP Paid Account",
                payment_method            = "voucher",
                processor                 = PaymentLog.VOUCHER,
                transaction_serial_number = 0
            )
            pl.save()

            # update user paid status if necessary
            if user_.is_paid != 1:
                user_.is_paid = 1
                user_.save()
                payment_notifications(user_, "redeemed", amount)
Ejemplo n.º 24
0
class Sharedfile(ModelQueryCache, Model):
    source_id = Property()
    user_id = Property()
    name = Property()
    title = Property()
    description = Property()
    source_url = Property()
    share_key = Property()
    content_type = Property()
    size = Property(default=0)
    # we set default to 0, since DB does not accept Null values
    like_count = Property(default=0)
    save_count = Property(default=0)
    view_count = Property(default=0)
    deleted = Property(default=0)
    parent_id = Property(default=0)
    original_id = Property(default=0)
    created_at = Property()
    updated_at = Property()
    activity_at = Property()

    def get_title(self, sans_quotes=False):
        """
        Returns title, escapes double quotes if sans_quotes is True, used
        for rendering title inside fields.
        """
        if self.title == '' or self.title == None:
            title = self.name
        else:
            title = self.title
        if sans_quotes:
            title = re.sub('"', '&quot;', title)
        return title

    def get_description(self, raw=False):
        """
        Returns desciption, escapes double quotes if sans_quotes is True, used
        for rendering description inside fields.
        """
        description = self.description
        if not description:
            description = ''

        if not raw:
            #description = escape.xhtml_escape(description)
            extra_params = 'target="_blank" rel="nofollow"'

            description = escape.linkify(description, True, 
                extra_params=extra_params)
            
            #re_hash = re.compile(r'#[0-9a-zA-Z+]*',re.IGNORECASE)
            #for iterator in re_hash.finditer(description):
            
            description = re.sub(r'(\A|\s)#(\w+)', r'\1<a href="/tag/\2">#\2</a>', description)

            description = description.replace('\n', '<br>')
        return description

    def save(self, *args, **kwargs):
        """
        Sets the dates before saving.
        """
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        ignore_tags = False

        #we dont want to set tags if this is a save from a shared file
        if 'ignore_tags' in kwargs and kwargs['ignore_tags']:
            ignore_tags = True

        if 'ignore_tags' in kwargs:
            del(kwargs['ignore_tags'])

        super(Sharedfile, self).save(*args, **kwargs)

        if ignore_tags:
            return
        
        # clear out all tags
        all_tagged_files = models.TaggedFile.where('sharedfile_id = %s', self.id)
        for tf in all_tagged_files:
            tf.deleted = 1
            tf.save()

        # extract tags
        tags = self.find_tags()
        for t in tags:
            tag = models.Tag.get("name = %s", t)
            if not tag:
                tag = models.Tag(name=t)
                tag.save()

            tagged_file = models.TaggedFile.get('sharedfile_id = %s and tag_id = %s', 
                self.id, tag.id)
            if tagged_file and tagged_file.deleted:
                tagged_file.deleted = 0
                tagged_file.save()
            else:
                tagged_file = models.TaggedFile(sharedfile_id=self.id, 
                    tag_id = tag.id, deleted=0)
                tagged_file.save()


    def can_save(self, user_check=None):
        """
        Can only save the file if the user is different.

        Also, if we haven't already saved it.
        """
        if options.readonly:
            return False
        if not user_check:
            return False
        if self.user_id == user_check.id:
            return False
        else:
            return True

    def can_delete(self, user_check=None):
        """
        Can only delete if the file belongs to the user.
        """
        if options.readonly:
            return False
        if not user_check:
            return False
        if self.user_id == user_check.id:
            return True
        else:
            return False

    def can_favor(self, user_check=None):
        """
        Can favor any image a user hasn't favorited, except
        if it's your image.
        """
        if options.readonly:
            return False
        if not user_check:
            return False
        if self.user_id == user_check.id:
            return False
        return not user_check.has_favorite(self)

    def can_unfavor(self, user_check=None):
        """
        Any use can favorite if they've already favored.
        """
        if options.readonly:
            return False
        if not user_check:
            return False
        if self.user_id == user_check.id:
            return False
        return user_check.has_favorite(self)

    def can_edit(self, user_check=None):
        """
        Checks if a user can edit the sharedfile. Can only edit the shardfile
        if the sharedfile belongs to them.
        """
        if options.readonly:
            return False
        if not user_check:
            return False
        if self.user_id == user_check.id:
            return True
        else:
            return False

    def save_to_shake(self, for_user, to_shake=None):
        """
        Saves this file to a user's shake, or to the to_shake
        if it is provided.
        """
        new_sharedfile = Sharedfile()
        new_sharedfile.user_id = for_user.id
        new_sharedfile.name = self.name
        new_sharedfile.title = self.title
        new_sharedfile.content_type = self.content_type
        new_sharedfile.source_url = self.source_url
        new_sharedfile.source_id = self.source_id
        new_sharedfile.parent_id = self.id
        new_sharedfile.description = self.description

        if self.original_id == 0:
            new_sharedfile.original_id = self.id
        else:
            new_sharedfile.original_id = self.original_id
        new_sharedfile.save(ignore_tags=True)
        new_sharedfile.share_key = base36encode(new_sharedfile.id)
        new_sharedfile.save(ignore_tags=True)

        if to_shake:
            shake_to_save = to_shake
        else:
            shake_to_save = for_user.shake()
        new_sharedfile.add_to_shake(shake_to_save)

        #create a notification to the sharedfile owner
        notification.Notification.new_save(for_user, self)

        calculate_saves.delay_or_run(self.id)
        return new_sharedfile

    def render_data(self, user=None, store_view=True):
        user_id = None
        if user:
            user_id = user.id
        source = self.sourcefile()
        oembed = escape.json_decode(source.data)
        if store_view:
            self.add_view(user_id)
        return oembed['html']

    def as_json(self, user_context=None):
        """
        If user_context is provided, adds a couple of fields to
        the returned dict representation, such as 'saved' and 'liked'.
        """
        u = self.user()
        source = self.sourcefile()

        json_object = {
            'user': u.as_json(),
            'nsfw' : source.nsfw_bool(),
            'pivot_id' : self.share_key,
            'sharekey' : self.share_key,
            'name' : self.name,
            'views' : self.view_count,
            'likes' : self.like_count,
            'saves' : self.save_count,
            'comments' : self.comment_count(),
            'width' : source.width,
            'height' : source.height,
            'title' : self.title,
            'description' : self.description,
            'posted_at' : self.created_at.replace(microsecond=0, tzinfo=None).isoformat() + 'Z',
            'permalink_page' : 'http://%s/p/%s' % (options.app_host, self.share_key)
        }

        if user_context:
            json_object['saved'] = bool(user_context.saved_sharedfile(self))
            json_object['liked'] = user_context.has_favorite(self)

        if(source.type == 'link'):
            json_object['url'] = self.source_url
        else:
            json_object['original_image_url'] = 'http://s.%s/r/%s' % (options.app_host, self.share_key)
        return json_object

    def sourcefile(self):
        """
        Returns sharedfile's Sourcefile.
        """
        return sourcefile.Sourcefile.get("id = %s", self.source_id)

    def can_user_delete_from_shake(self, user, from_shake):
        """
        A user can delete a sharedfile from a shake if they are the owner of the sharedfile
        or if they are the shake owner.
        """
        if options.readonly:
            return False
        if self.user_id == user.id:
            return True
        if from_shake.is_owner(user):
            return True
        return False

    def delete_from_shake(self, from_shake):
        """
        Removes a file from a shake.  Make sure we find the shakesharedfile entry and only mark it as
        deleted if it's in another shake (2 or more shakes when this action was initiated).
        """
        if options.readonly:
            return False
        ssf = shakesharedfile.Shakesharedfile.get("shake_id = %s and sharedfile_id = %s", from_shake.id, self.id)
        if not ssf:
            return False
        ssf.deleted = 1
        if ssf.save():
            return True
        else:
            return False

    def add_to_shake(self, to_shake):
        """
        Takes any shake and adds this shared file to it.
            - TODO: need to check if has permission
        """
        if options.readonly:
            return False
        ssf = shakesharedfile.Shakesharedfile.get("shake_id = %s and sharedfile_id = %s", to_shake.id, self.id)
        if not ssf:
            ssf = shakesharedfile.Shakesharedfile(shake_id=to_shake.id, sharedfile_id=self.id)
        ssf.deleted = 0
        ssf.save()
        if ssf.saved():
            add_posts.delay_or_run(shake_id=to_shake.id, sharedfile_id=self.id, sourcefile_id=self.source_id)

    def shakes(self):
        """
        The shakes this file is in.
        """
        select = """
            select shake.* from shake
            left join shakesharedfile on
            shakesharedfile.shake_id = shake.id
            where shakesharedfile.sharedfile_id = %s
            and shakesharedfile.deleted = 0;
        """
        return shake.Shake.object_query(select, self.id)

    def user(self):
        """
        Returns sharedfile's user.
        """
        return user.User.get("id = %s", self.user_id)

    def parent(self):
        """
        Returns the parent object if it's set, otherwise returns None.
        """
        if not bool(self.parent_id):
            return None
        return self.get("id = %s", self.parent_id)

    def original(self):
        """
        Returns the original object if it's set, otherwise returns None.
        """
        if not bool(self.original_id):
            return None
        return self.get("id = %s", self.original_id)

    def parent_user(self):
        """
        If a sharedfile has a parent_sharedfile_id set, returns user of the
        parent sharedfile.
        """
        parent = self.parent()
        if not parent:
            return None
        return parent.user()

    def original_user(self):
        """
        If a sharedfile has an original_id, this returns the user who
        originally shared that file
        """
        original = self.original()
        if not original:
            return None
        return original.user()

    def delete(self):
        """
        Sets the deleted flag to 1 and saves to DB.
        """
        if options.readonly:
            return False

        self.deleted = 1;
        self.save()

        tags = models.TaggedFile.where('sharedfile_id = %s', self.id)
        for tag in tags:
            tag.deleted = 1
            tag.save()

        delete_posts.delay_or_run(sharedfile_id=self.id)

        if self.original_id > 0:
            calculate_saves.delay_or_run(self.original_id)
        if self.parent_id > 0:
            calculate_saves.delay_or_run(self.original_id)

        #mute conversations
        conversations = conversation.Conversation.where('sharedfile_id = %s', self.id)
        [c.mute() for c in conversations]

        ssfs = shakesharedfile.Shakesharedfile.where('sharedfile_id = %s', self.id)
        [ssf.delete() for ssf in ssfs]


    def add_view(self, user_id=None):
        """
        Increments a view for the image.
        """
        if options.readonly:
            return False

        if not user_id:
            user_id = 0
        self.connection.execute("INSERT INTO fileview (user_id, sharedfile_id, created_at) VALUES (%s, %s, NOW())", user_id, self.id)

    def pretty_created_at(self):
        """
        A friendly version of the created_at date.
        """
        return pretty_date(self.created_at)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow()
        self.updated_at = datetime.utcnow()

    def update_view_count(self):
        """
        Update view_count field for current sharedfile.
        """
        self.update_attribute('view_count', self.calculate_view_count())

    def calculate_view_count(self):
        """
        Calculate count of all views for the sharedfile.
        """
        count = fileview.Fileview.query(
            """SELECT count(*) AS result_count FROM fileview
               WHERE sharedfile_id = %s and user_id != %s""", self.id, self.user_id)
        return int(count[0]['result_count'])

    def livish_view_count(self):
        """
        If a file is recent, show its live view count.
        """
        if datetime.utcnow() - self.created_at < timedelta(hours=24):
            return self.calculate_view_count()
        else:
            # if a file is not recent and also has zero
            # then try to pull a live count anyway.
            if self.view_count == 0:
                return self.calculate_view_count()
            else:
                return self.view_count

    def saves(self):
        """
        Retrieve all saves of this file.
        """
        original =  self.where("original_id = %s and deleted = 0", self.id)
        if len(original) > 0:
            return original
        else:
            return self.where("parent_id = %s and deleted = 0", self.id)

    def favorites(self):
        """
        Retrieve all saves of this file.
        """
        return favorite.Favorite.where("sharedfile_id = %s and deleted = 0 ORDER BY id", self.id)

    def calculate_save_count(self):
        """
        Count of all saves for the images.  If the file is the original for other
        sharedfiles, then the save count is the total of all files where it's the
        original.  If the file is not an original, only count direct saves, ala
        parent_id.
        """
        original =  self.where_count("original_id = %s and deleted = 0", self.id)
        if original > 0:
            return original
        else:
            return self.where_count("parent_id = %s and deleted = 0", self.id)

    def calculate_like_count(self):
        """
        Count of all favorites, excluding deleted favorites.
        """
        return favorite.Favorite.where_count("sharedfile_id = %s and deleted = 0", self.id)

    def comment_count(self):
        """
        Counts all comments, excluding deleted favorites.
        """
        return comment.Comment.where_count("sharedfile_id = %s and deleted = 0", self.id)

    def comments(self):
        """
        Select comments for a sharedfile.
        """
        return comment.Comment.where('sharedfile_id=%s and deleted = 0', self.id)

    def feed_date(self):
        """
        Returns a date formatted to be included in feeds
        e.g., Tue, 12 Apr 2005 13:59:56 EST
        """
        return self.created_at.strftime("%a, %d %b %Y %H:%M:%S %Z")

    def thumbnail_url(self):
        return s3_authenticated_url(options.aws_key, options.aws_secret, options.aws_bucket, \
            file_path="thumbnails/%s" % (self.sourcefile().thumb_key), seconds=3600)

    def small_thumbnail_url(self):
        """

        """
        return s3_authenticated_url(options.aws_key, options.aws_secret, options.aws_bucket, \
            file_path="smalls/%s" % (self.sourcefile().small_key), seconds=3600)

    def type(self):
        source = sourcefile.Sourcefile.get("id = %s", self.source_id)
        return source.type

    def set_nsfw(self, set_by_user):
        """
        Process a request to set the nsfw flag on the sourcefile.  Also logs the
        the user, sharedfile and sourcefile in the NSFWLog table.
        """
        sourcefile = self.sourcefile()
        log_entry = models.nsfw_log.NSFWLog(user_id=set_by_user.id, sharedfile_id=self.id,
                                            sourcefile_id=sourcefile.id)
        log_entry.save()
        if sourcefile.nsfw == 0:
            sourcefile.update_attribute('nsfw', 1)

    def find_tags(self):    
        if not self.description:
            return []
        candidates = set(part[1:] for part in self.description.split() if part.startswith('#'))
        candidates = [re.search(r'[a-zA-Z0-9]+', c).group(0) for c in candidates]
        return set([c.lower() for c in candidates if len(c) < 21])

    def tags(self):
        #return models.TaggedFile.where("sharedfile_id = %s and deleted = 0", self.id)
        return [models.Tag.get('id = %s', tf.tag_id) for tf in models.TaggedFile.where("sharedfile_id = %s and deleted = 0", self.id)]

    @classmethod
    def from_subscriptions(self, user_id, per_page=10, before_id=None, after_id=None):
        """
        Pulls the user's timeline, can key off and go backwards (before_id) and forwards (after_id)
        in time to pull the per_page amount of posts.  Always returns the files in reverse
        chronological order.

        We split out the join from the query and only pull the sharedfile_id because MySQL's
        query optimizer does not use the index consistently. -- IK
        """
        constraint_sql = ""
        order = "desc"
        if before_id:
            constraint_sql = "AND post.sharedfile_id < %s" % (int(before_id))
        elif after_id:
            order = "asc"
            constraint_sql = "AND post.sharedfile_id > %s" % (int(after_id))

        select = """SELECT sharedfile_id, shake_id FROM post
                    WHERE post.user_id = %s
                    AND post.seen = 0
                    AND post.deleted = 0
                    %s
                    ORDER BY post.sharedfile_id %s limit %s, %s""" % (int(user_id), constraint_sql, order, 0, per_page)

        posts = self.query(select)
        results = []
        for post in posts:
            sf = Sharedfile.get('id=%s', post['sharedfile_id'])
            sf.shake_id = post['shake_id']
            results.append(sf)
        if order == "asc":
            results.reverse()
        return results


    @classmethod
    def subscription_time_line(self, user_id, page=1, per_page=10):
        """
        DEPRACATED: We no longer paginate like this. instead we use Sharedfile.from_subscription
        """
        limit_start = (page-1) * per_page
        select = """SELECT sharedfile.* FROM sharedfile, post
                  WHERE post.user_id = %s
                  AND post.sharedfile_id = sharedfile.id
                  AND post.seen = 0
                  AND post.deleted = 0
                  ORDER BY post.created_at desc limit %s, %s""" % (int(user_id), int(limit_start), per_page)
        return self.object_query(select)


    @classmethod
    def favorites_for_user(self, user_id, before_id=None, after_id=None, per_page=10):
        """
        A user likes (i.e. Favorite).
        """
        constraint_sql = ""
        order = "desc"

        if before_id:
            constraint_sql = "AND favorite.id < %s" % (int(before_id))
        elif after_id:
            order = "asc"
            constraint_sql = "AND favorite.id > %s" % (int(after_id))

        select = """SELECT sharedfile.*, favorite.id as favorite_id FROM sharedfile
                    left join favorite
                    on favorite.sharedfile_id = sharedfile.id
                    WHERE favorite.user_id = %s
                    AND favorite.deleted = 0
                    %s
                    GROUP BY sharedfile.source_id
                    ORDER BY favorite.id %s limit 0, %s""" % (int(user_id), constraint_sql, order, per_page)
        files = self.object_query(select)
        if order == "asc":
            files.reverse()
        return files


    @classmethod
    def get_by_share_key(self, share_key):
        """
        Returns a Sharedfile by its share_key. Deleted files don't get returned.
        """
        sharedfile_id = base36decode(share_key)
        return self.get("id = %s and deleted = 0", sharedfile_id)

    @classmethod
    def incoming(self, before_id=None, after_id=None, per_page=10, filter=True):
        """
        Fetches the per_page amount of incoming files.  Filters out any files where
        the user is marked as nsfw.
        """
        constraint_sql = ""
        order = "desc"
        nsfw_sql = ""

        if before_id:
            constraint_sql = "AND sharedfile.id < %s" % (int(before_id))
        elif after_id:
            order = "asc"
            constraint_sql = "AND sharedfile.id > %s" % (int(after_id))

        nsfw_sql = "AND user.nsfw = 0"

        select = """SELECT sharedfile.* FROM sharedfile, user
                    WHERE sharedfile.deleted = 0
                    AND sharedfile.parent_id = 0
                    AND sharedfile.original_id = 0
                    AND sharedfile.user_id = user.id
                    %s
                    %s
                    ORDER BY id %s LIMIT %s""" % (nsfw_sql, constraint_sql, order, per_page)
        files = self.object_query(select)
        if order == "asc":
            files.reverse()
        return files


    @staticmethod
    def get_sha1_file_key(file_path):
        try:
            fh = open(file_path, 'r')
            file_data = fh.read()
            fh.close()
            h = hashlib.sha1()
            h.update(file_data)
            return h.hexdigest()
        except Exception as e:
            return None

    @staticmethod
    def create_from_file(file_path, file_name, sha1_value, content_type, user_id, title=None, shake_id=None, skip_s3=None):
        """
        TODO: Must only accept acceptable content-types after consulting a list.
        """
        if len(sha1_value) <> 40:
            return None

        if user_id == None:
            return None

        if content_type not in ['image/gif', 'image/jpeg', 'image/jpg', 'image/png']:
            return None

        # If we have no shake_id, drop in user's main shake. Otherwise, validate that the specififed
        # shake is a group shake that the user has permissions for.
        if not shake_id:
            destination_shake = shake.Shake.get('user_id = %s and type=%s', user_id, 'user')
        else:
            destination_shake = shake.Shake.get('id=%s', shake_id)
            if not destination_shake:
                return None
            if not destination_shake.can_update(user_id):
                return None

        sf = sourcefile.Sourcefile.get_from_file(file_path, sha1_value, skip_s3=skip_s3)

        if sf:
            shared_file = Sharedfile(user_id = user_id, name=file_name, content_type=content_type, source_id=sf.id, title=title, size=path.getsize(file_path))
            shared_file.save()
            if shared_file.saved():
                shared_file.share_key = base36encode(shared_file.id)
                shared_file.save()
                shared_file.add_to_shake(destination_shake)
                return shared_file
            else:
                return None
        else:
            return None
Ejemplo n.º 25
0
class Notification(Model):
    sender_id   = Property()
    receiver_id = Property()
    action_id   = Property()
    type        = Property() # favorite, save, subscriber
    deleted     = Property(default=0)
    created_at  = Property()
    
    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Notification, self).save(*args, **kwargs)
    
    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")            

    def delete(self):
        self.deleted = 1
        self.save()
        
    def sender(self):
        return user.User.get("id=%s", self.sender_id)
        
    def receiver(self):
        return user.User.get("id=%s", self.receiver_id)
    
    def related_object(self):
        """
        Return the object this notification relates to. In the case of a favorite
        or a like, it should return shared file. In the case of a follow, the user
        that followed.
        """
        if self.type in ['favorite', 'save']:
            return sharedfile.Sharedfile.get("id=%s", self.action_id)
        elif self.type == 'comment':
            return comment.Comment.get("id=%s", self.action_id)
        elif self.type in ['invitation', 'invitation_request', 'invitation_approved']:
            return shake.Shake.get("id=%s", self.action_id)
        elif self.type == 'subscriber':
            subscription_ = subscription.Subscription.get("id = %s", self.action_id)
            return subscription_.shake()
        else:
            return user.User.get("id = %s", self.sender_id)
    
    @classmethod
    def invitation_to_shake_for_user(self, shake, user):
        """
        Returns outstanding invitation notification to a shake for a user.
        """
        return self.get("type = 'invitation' and action_id = %s and receiver_id = %s and deleted = 0 LIMIT 1", shake.id, user.id)

    @classmethod
    def mentions_for_user(self, user_id, page=1, per_page=10):
        limit_start = (page-1) * per_page
        select = """
          SELECT * from notification 
            WHERE receiver_id = %s
            AND type = 'mention'
            ORDER BY id desc
          LIMIT %s, %s
        """ % (user_id, limit_start, per_page)
        notifications = self.object_query(select)
        return notifications 

    @classmethod
    def mentions_for_user_count(self, user_id):
        select = """
          SELECT count(id) as count from notification 
            WHERE receiver_id = %s
            AND type = 'mention'
        """ % (user_id)
        result = self.query(select)
        return result[0]['count'] 

    @classmethod
    def count_for_user_by_type(self, user_id, _type='invitation'):
        """
        Count of outstanding notifications for a specified user, by 
        notification type.
        """
        return self.where_count("receiver_id = %s and type = %s and deleted = 0", user_id, _type)

    @classmethod
    def display_for_user(cls, user):
        """
        Returns a data structure used in display all open notifications
        for a specified user.  Collapses likes and follows into one
        notification if they reference same image.
        """
        notifications = { 'like' : {'count' : 0, 'items' : {}}, 
                          'save' : {'count' : 0, 'items' : {}}, 
                          'follow' : [] , 
                          'comment' : [], 
                          'mention': [], 
                          'invitation':[],
                          'invitations': [], # TODO: kill this ambiguity - IK
                          'invitation_request' : [],
                          'invitation_approved' : []
                        }
        for notification in cls.for_user(user):
            sender = notification.sender()
            related_object = notification.related_object()
            _notification = {'sender' : sender, 'related_object' : related_object, 'id' : notification.id}
            
            if notification.type == 'favorite':
                if not notifications['like']['items'].has_key(related_object.id):
                    notifications['like']['items'][related_object.id] = []
                notifications['like']['items'][related_object.id].append(_notification)
                notifications['like']['count'] += 1
                
            elif notification.type == 'subscriber':
                _notification['post_name_text'] = " is now following " + related_object.display_name(user)
                notifications['follow'].append(_notification)
                
            elif notification.type == 'save':
                if not notifications['save']['items'].has_key(related_object.id):
                    notifications['save']['items'][related_object.id] = []
                notifications['save']['items'][related_object.id].append(_notification)
                notifications['save']['count'] += 1

            elif notification.type == 'comment':
                notifications['comment'].append(_notification)

            elif notification.type == 'mention':
                notifications['mention'].append(_notification)

            elif notification.type == 'invitation':
                notifications['invitation'].append(_notification)
            
            elif notification.type == 'invitation_request':
                notifications['invitation_request'].append(_notification)
            
            elif notification.type == 'invitation_approved':
                notifications['invitation_approved'].append(_notification)
            
        #for invitation_ in invitation.Invitation.by_user(user):
        #    notifications['invitations'].append(invitation_.email_address)
        return notifications
        
    @classmethod
    def for_user(cls, user, deleted=False):
        """
        Notifications for user. Defaults to open,
        i.e. non-deleted notifications.
        """
        one_week_ago = datetime.utcnow() - timedelta(days=7)
        return cls.where("receiver_id = %s and deleted = %s and created_at > %s order by created_at desc", 
                                   user.id, deleted, one_week_ago.strftime("%Y-%m-%d %H:%M:%S"))

    @classmethod
    def for_user_count(cls, user, deleted=False):
       """
       Count of all notifications for user.  Defaults to open,
       i.e. non-deleted notifications.
       """
       one_week_ago = datetime.utcnow() - timedelta(days=7)
       return cls.where_count("receiver_id = %s and deleted = %s and created_at > %s order by created_at desc", 
                                        user.id, deleted, one_week_ago.strftime("%Y-%m-%d %H:%M:%S"))

    @staticmethod
    def new_favorite(sender, sharedfile):
        """
        sender - user who created the favorite
        sharedfile - the file that was favorited. 
            the receiver in this case is the sharedfile.user_id
        """
        n = Notification(sender_id=sender.id, receiver_id=sharedfile.user_id, action_id=sharedfile.id, type='favorite')
        n.save()
        return n
    
    @staticmethod
    def new_save(sender, sharedfile):
        """
        sender - user who created the sharedfile
        sharedfile - the originating file that was saved from.
            the receiver in this case is the sharedfile.user_id
        """
        n = Notification(sender_id=sender.id, receiver_id=sharedfile.user_id, action_id=sharedfile.id, type='save')
        n.save()
        return n

    @staticmethod
    def new_comment(comment):
        """
        sender - person leaving the comment
        sharedfile - the file that was commented on
        comment - the comment being left
        """
        sf = sharedfile.Sharedfile.get('id=%s', comment.sharedfile_id)
        n = Notification(sender_id=comment.user_id, receiver_id=sf.user_id, action_id=comment.id, type='comment')
        n.save()
        return n
    
    @staticmethod
    def new_comment_like(comment, sender):
        """
        sender - person doing the liking
        comment - the comment that was liked
        receiver - the person who received the like
        """
        n = Notification(sender_id=sender.id, receiver_id=comment.user_id,
                action_id=comment.id, type='comment_like')
        n.save()
        return n

    @staticmethod
    def new_mention(receiver, comment):
        """
        receiver - user who is mentioned
        comment - the comment it appeared in
        """
        n = Notification(sender_id=comment.user_id, receiver_id=receiver.id, action_id=comment.id, type='mention')
        n.save()
        return n
        
    @staticmethod
    def new_invitation_request_accepted(sender, receiver, shake):
        """
        sender - user who granted invitation
        receiver - user who was invited in
        shake
        """
        n = Notification(sender_id=sender.id, receiver_id=receiver.id, action_id=shake.id, type='invitation_approved')
        n.save()

    @staticmethod
    def new_invitation_to_shake(sender, receiver, action_id):
        """
        sender - user making request
        receiver - user who owns the shake
        action_id - the shake_id
        """
        the_shake = shake.Shake.get('id=%s', action_id)
        
        n = Notification(sender_id=sender.id, receiver_id=receiver.id, action_id=action_id, type='invitation_request')
        text_message = """Hi, %s.
%s has requested to join "%s". This means they will be able to put files into the "%s" shake.

If you want to let %s do this, simply visit your shake and approve the request:

http://mltshp.com/%s

You can also ignore the request by deleting the notification.
""" % (receiver.display_name(), sender.display_name(), the_shake.display_name(), the_shake.display_name(), sender.display_name(), the_shake.name)
        html_message = """<p>Hi, %s.</p>
<p><a href="http://mltshp.com/user/%s">%s</a> has requested to join "<a href="http://mltshp.com/%s">%s</a>". This means they will be able to put files into the "%s" shake.</p>

<p>If you want to let %s do this, simply visit your shake and approve the request:</p>

<p><a href="http://mltshp.com/%s">http://mltshp.com/%s</a></p>

You can also ignore the request by deleting the notification.
""" % (receiver.display_name(), sender.name, sender.display_name(), the_shake.name, the_shake.display_name(), the_shake.display_name(), sender.display_name(), the_shake.name, the_shake.name)

        n.save()
        if not receiver.disable_notifications and not options.debug:
            pm = postmark.PMMail(api_key=options.postmark_api_key, 
                sender="*****@*****.**", to=receiver.email, 
                subject="%s has requested an invitation to %s!" % (sender.display_name(), the_shake.display_name()), 
                text_body=text_message, 
                html_body=html_message)
            pm.send()
        return n

    @staticmethod
    def new_invitation(sender, receiver, action_id):
        """
        sender - user who created invitation
        receiver - user who is being invited
        action_id - the shake_id
        """
        new_shake = shake.Shake.get('id = %s', action_id)
        n = Notification(sender_id=sender.id, receiver_id=receiver.id, action_id=action_id, type='invitation')
        text_message = """Hi, %s.
We wanted to let you know %s has invited you to join "%s". Being a member of "%s" means you can upload files to the shake with others who are members.

You can agree to join here:

http://mltshp.com/%s

If you do join you'll notice a new shake name when you upload or save files.
""" % (receiver.name, sender.display_name(), new_shake.display_name(), new_shake.display_name(), new_shake.name)
        html_message = """<p>Hi, %s.</p>
<p>We wanted to let you know <a href="http://mltshp.com/user/%s">%s</a> has invited you to join "<a href="http://mltshp.com/%s">%s</a>". 
Being a member of "<a href="http://mltshp.com/%s">%s</a>" means you can upload files to the shake along with others who are members.</p>

<p>
You can agree to join here:
</p>
<p>
<a href="http://mltshp.com/%s">http://mltshp.com/%s</a>
</p>
<p>
If you do join you'll notice a new shake name when you upload or save files.
</p>
""" % (receiver.name, sender.name, sender.display_name(), new_shake.name, new_shake.display_name(), new_shake.name, new_shake.display_name(), 
            new_shake.name, new_shake.name)
            
        n.save()

        if not receiver.disable_notifications and not options.debug:
            pm = postmark.PMMail(api_key=options.postmark_api_key, 
                sender="*****@*****.**", to=receiver.email, 
                subject="%s has invited you to join %s!" % (sender.display_name(), new_shake.display_name()), 
                text_body=text_message, 
                html_body=html_message)
            pm.send()
        return n
        
    @staticmethod
    def new_subscriber(sender, receiver, action_id):
        """
        sender - user who created the subscription
        receiver - user who is being followed (their shake is)
        action_id - the subscription id
        """
        new_subscription = subscription.Subscription.get('id = %s', action_id)
        target_shake = shake.Shake.get('id = %s', new_subscription.shake_id)
        subscription_line = ""
        if target_shake.type == 'group':
            subscription_line = " called '%s'" % (target_shake.name)
        
        n = Notification(sender_id=sender.id, receiver_id=receiver.id, action_id=action_id, type='subscriber')

        text_message = """Hi, %s.
We wanted to let you know %s is now following your shake%s. If you want to check out their shake you can do so here:

http://mltshp.com/user/%s

You can change your preferences for receiving notifications on your settings page: http://mltshp.com/account/settings

Have a good day.
- MLTSHP
""" % (receiver.name, sender.name, subscription_line, sender.name)
        html_message = """<p>Hi, %s.</p>
<p>                        
We wanted to let you know <a href="http://mltshp.com/user/%s">%s</a> is now following your shake%s. If you want to check out their shake you can do so here:
</p>
<p>
<a href="http://mltshp.com/user/%s">http://mltshp.com/user/%s</a>
</p>
<p>
You can change your preferences for receiving notifications on your settings page: <a href="http://mltshp.com/account/settings">http://mltshp.com/account/settings</a>.
</p>
<p>
Have a good day.<br>
- MLTSHP
</p>
""" % (receiver.name, sender.name, sender.name, subscription_line, sender.name, sender.name)

        n.save()
        
        if not receiver.disable_notifications and not options.debug:
            pm = postmark.PMMail(api_key=options.postmark_api_key, 
                sender="*****@*****.**", to=receiver.email, 
                subject="%s is now following your shake!" % (sender.name), 
                text_body=text_message, 
                html_body=html_message)
            pm.send()
        return n
        
    @staticmethod
    def send_shake_member_removal(former_shake, former_member):
        """
        Sends an email informing someone they were removed from a shake.
        """
        text_message = """Hi %s.
This is a note to let you know you were removed as a member of "%s". 

You can still follow the shake, but the editor has decided to remove you as an image contributor.

Editors remove members for various reasons. Either the shake is shutting down or they want to do something different with it.

Thank you,
- MLTSHP
""" % (former_member.display_name(), former_shake.display_name())
        if not former_member.disable_notifications and not options.debug:
            pm = postmark.PMMail(api_key=options.postmark_api_key, 
                sender="*****@*****.**", to=former_member.email, 
                subject="Removal from %s shake" % (former_shake.display_name()), 
                text_body=text_message)
            pm.send()
Ejemplo n.º 26
0
class Comment(Model):
    user_id = Property()
    sharedfile_id = Property()
    body = Property()
    deleted = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Comment, self).save(*args, **kwargs)

    def on_create(self):
        """
        Creates a notification for the user that owns the shared file
        (does not create a new notification if you are the user)
        """
        sf = self.sharedfile()
        if self.user_id != sf.user_id:
            notification.Notification.new_comment(self)

        #creates a conversation for a user if one doesn't exist.
        existing_conversation = conversation.Conversation.get(
            'user_id = %s and sharedfile_id = %s', self.user_id,
            self.sharedfile_id)
        if not existing_conversation:
            new_conversation = conversation.Conversation(
                user_id=self.user_id, sharedfile_id=self.sharedfile_id)
            new_conversation.save()

        #creates a conversation for sharedfile.user_id if one doesn't exist
        existing_conversation = conversation.Conversation.get(
            'user_id = %s and sharedfile_id=%s', sf.user_id,
            self.sharedfile_id)
        if not existing_conversation:
            new_conversation = conversation.Conversation(
                user_id=sf.user_id, sharedfile_id=self.sharedfile_id)
            new_conversation.save()

        # update the SF activity_at
        sf.activity_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        sf.save()

        # find any mentions and create notifciations
        mentions = self.extract_mentions()
        for u in mentions:
            notification.Notification.new_mention(u, self)

    def as_json(self):
        return {
            'body':
            self.body,
            'user':
            self.user().as_json(),
            'posted_at':
            self.created_at.replace(microsecond=0, tzinfo=None).isoformat() +
            'Z',
        }

    def sharedfile(self):
        return sharedfile.Sharedfile.get('id = %s', self.sharedfile_id)

    def chopped_body(self):
        """
        Returns a comment that has its HTML removed, shortened to 15 words, and if it doesn't end in a period, add ...
        """
        new_body = ''.join(BeautifulSoup(self.body).findAll(text=True))
        new_body = new_body.replace('\n', '')
        body_parts = new_body.split(' ')
        new_body = body_parts[:12]
        new_body = " ".join(new_body)
        if not new_body.endswith(('.', '!', '?')):
            new_body = "%s&hellip;" % new_body
        return new_body

    def user(self):
        """
        Returns comment's user.
        """
        return user.User.get("id = %s", self.user_id)

    def pretty_created_at(self):
        """
        A friendly version of the created_at date.
        """
        return pretty_date(self.created_at)

    def body_formatted(self):
        """
        An escaped and formatted body of the comment with \n replaced by HTML <br>
        """
        #body = escape.xhtml_escape(self.body)
        #print body
        #body = escape.linkify(body, True) #someday?
        #for now use Bleach
        #bl = Bleach()
        #body = bl.linkify(body, nofollow=True)
        #body = body.replace('</a>/', '/</a>')
        #body = body.replace('<a href=', '<a target="_blank" href=')
        body = escape.linkify(self.body,
                              True,
                              extra_params='rel="nofollow" target="_blank"')
        body = body.replace('\n', '<br>')
        return body

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow()
        self.updated_at = datetime.utcnow()

    def extract_mentions(self):
        """
        This method extracts all the users mentioned in a body.
        *ONLY* returns valid users and only returns one user per comment.
        """
        user_list = []
        if self.body == None or self.body == '':
            return user_list
        matches = re.findall('@([A-Za-z0-9_\-]+)', self.body)
        for match in matches:
            matching_user = user.User.get('name=%s', match)
            if matching_user and matching_user.id not in [
                    u.id for u in user_list
            ]:
                user_list.append(matching_user)
        return user_list

    def can_user_delete(self, user):
        """
        Determines whether a passed in user can delete the current
        comment. Only the person that owns the comment or the person
        that owns the file the comment references can delete a comment.
        """
        if options.readonly:
            return False
        if self.user_id == user.id:
            return True
        if self.sharedfile().user_id == user.id:
            return True
        return False

    def delete(self):
        self.deleted = 1
        self.save()

    @classmethod
    def add(self, user=None, sharedfile=None, body=None):
        """
        Creates a comment and returns it, or returns None if some conditions
        are not met.  Sets a user to be restricted if they exhibit spammy
        behavior.
        """
        if not user or not sharedfile or not body:
            return None

        if user.restricted:
            return None

        body = body.strip()
        if len(body) == 0:
            return None

        now = datetime.utcnow()
        if user.created_at > (now - timedelta(hours=24)):
            if user.sharedfiles_count() == 0 and user.likes_count() == 0:
                if Comment.where_count("user_id = %s", user.id) >= 1:
                    user.restricted = 1
                    user.save()
                    all_comments = Comment.where('user_id=%s', user.id)
                    for this_comment in all_comments:
                        this_comment.delete()

        comment = Comment(user_id=user.id,
                          sharedfile_id=sharedfile.id,
                          body=body)
        comment.save()
        return comment
Ejemplo n.º 27
0
class Conversation(Model):
    user_id = Property()
    sharedfile_id = Property()
    muted = Property(default=0)
    created_at = Property()
    updated_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Conversation, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def mute(self):
        self.muted = 1
        self.save()

    def sharedfile(self):
        """
        Associates sharedfile.
        """
        return sharedfile.Sharedfile.get("id=%s", self.sharedfile_id)

    def relevant_comments(self):
        """
        Returns comments to display for the user's conversation.  Returns
        all comments that aren't deleted.
        """
        return comment.Comment.where('sharedfile_id = %s and deleted = 0',
                                     self.sharedfile_id)

    @classmethod
    def for_user(self, user_id, type='all', page=1, per_page=10):
        limit_start = (page - 1) * per_page
        filtering_by = ""
        if type == 'myfiles':
            filtering_by = "AND sharedfile.user_id = conversation.user_id"
        elif type == 'mycomments':
            filtering_by = "AND sharedfile.user_id != conversation.user_id"
        select = """
          SELECT conversation.* from conversation, sharedfile
          WHERE conversation.user_id = %s
            AND conversation.muted = 0
            AND sharedfile.id = conversation.sharedfile_id
            %s
            ORDER BY sharedfile.activity_at desc
          limit %s, %s
        """ % (user_id, filtering_by, limit_start, per_page)
        conversations = self.object_query(select)
        return conversations

    @classmethod
    def for_user_count(self, user_id, type='all'):
        filtering_by = ''
        if type == 'myfiles':
            filtering_by = "AND sharedfile.user_id = conversation.user_id"
        elif type == 'mycomments':
            filtering_by = "AND sharedfile.user_id != conversation.user_id"
        select = """
          SELECT count(conversation.id) as count from conversation, sharedfile
          WHERE conversation.user_id = %s
            AND sharedfile.id = conversation.sharedfile_id
            AND conversation.muted = 0
            %s
        """ % (user_id, filtering_by)
        result = self.query(select)
        return result[0]['count']
Ejemplo n.º 28
0
class PaymentLog(Model):
    user_id                   = Property()
    status                    = Property()
    reference_id              = Property()
    transaction_id            = Property()
    operation                 = Property()
    transaction_date          = Property()
    next_transaction_date     = Property()
    buyer_email               = Property()
    buyer_name                = Property()
    recipient_email           = Property()
    recipient_name            = Property()
    payment_reason            = Property()
    transaction_serial_number = Property()
    subscription_id           = Property()
    payment_method            = Property()
    transaction_amount        = Property()
    processor                 = Property(default=0)
    created_at                = Property()
    updated_at                = Property()

    #AMAZON  = 0
    #TUGBOAT = 1
    VOUCHER = 2
    STRIPE  = 3

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(PaymentLog, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    @staticmethod
    def last_payments(count=3, user_id=None):
        """
        Grabs the last <<count>> payments sorted by id.
        """
        if not user_id:
            return []
        if count <= 0:
            return []
            
        return PaymentLog.where('user_id = %s AND status IN (%s, %s, %s, %s) ORDER BY id desc LIMIT %s',
             user_id, 'PS', 'payment', 'captured', 'credit', count)
Ejemplo n.º 29
0
class Shake(ModelQueryCache, Model):
    user_id = Property(name='user_id')
    type = Property(name='type', default='user')
    title = Property(name='title')
    name = Property(name='name')
    image = Property(name='image', default=0)
    description = Property(name='description')
    recommended = Property(name='recommended', default=0)
    featured = Property(name='featured', default=0)
    shake_category_id = Property(name='shake_category_id', default=0)
    deleted = Property(default=0)
    created_at = Property(name='created_at')
    updated_at = Property(name='updated_at')

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        if self.type == 'group':
            self._validate_title()
            self._validate_name()
        if len(self.errors) > 0:
            return False
        return super(Shake, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
        self.updated_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    def delete(self):
        """
        Sets the deleted flag to 1 and saves to DB.
        """
        if options.readonly:
            return False

        self.deleted = 1
        self.save()

    def as_json(self, extended=False):
        base_dict = {
            'id':
            self.id,
            'name':
            self.display_name(),
            'url':
            'https://%s%s' % (options.app_host, self.path()),
            'thumbnail_url':
            self.thumbnail_url(),
            'description':
            self.description,
            'type':
            self.type,
            'created_at':
            self.created_at.replace(microsecond=0, tzinfo=None).isoformat() +
            'Z',
            'updated_at':
            self.updated_at.replace(microsecond=0, tzinfo=None).isoformat() +
            'Z'
        }

        if extended:
            base_dict['owner'] = self.owner().as_json()

        return base_dict

    def owner(self):
        return user.User.get('id=%s', self.user_id)

    def on_create(self):
        #create a subscription for the user if this is a group
        if self.type == 'group':
            new_sub = subscription.Subscription(user_id=self.user_id,
                                                shake_id=self.id)
            new_sub.save()

    def page_image(self):
        """
        Return's users profile image if it's a user shake or the shake image.
        If no image, returns None.
        """
        if self.type == 'user':
            return self.owner().profile_image_url()
        else:
            if self.image:
                return "https://%s.s3.amazonaws.com/account/%s/shake_%s.jpg" % (
                    options.aws_bucket, self.user_id, self.name)
            else:
                return None

    def thumbnail_url(self):
        if self.type == 'user':
            return self.owner().profile_image_url(include_protocol=True)
        else:
            if self.image:
                return "https://%s.s3.amazonaws.com/account/%s/shake_%s_small.jpg" % (
                    options.aws_bucket, self.user_id, self.name)
            else:
                if options.app_host == "mltshp.com":
                    return "https://%s/static/images/default-icon-venti.svg" % options.cdn_ssl_host
                else:
                    return "http://%s/static/images/default-icon-venti.svg" % options.app_host

    def path(self):
        """
        Return the path for the shake with a leading slash.
        """
        if self.type == 'user':
            return '/user/%s' % self.owner().name
        else:
            return '/%s' % self.name

    def display_name(self, user=None):
        """
        If it's a user shake returns the user's "name", otherwise return.
        the shake's title.  If a user is passed in and it's the user's main
        shake, it returns "Your Shake".
        """
        if self.type == 'user':
            if user and user.id == self.user_id:
                return "Your Shake"
            return self.owner().display_name()
        else:
            return self.title

    def can_update(self, user_id):
        if options.readonly:
            return False

        if user_id == self.user_id:
            return True

        #do some checking in the shakemanager table to see if this person can
        #contribute to this shake.
        existing_manager = shakemanager.ShakeManager.get(
            "user_id = %s and shake_id = %s and deleted = 0", user_id, self.id)
        if existing_manager:
            return True
        return False

    def _validate_title(self):
        """
        Title can't be blank.
        """
        if self.title == None or self.title == "":
            self.add_error('title', "Title can't be blank.")
            return False

    def _validate_name(self):
        """
        Check that the name being used is valid and not a reserved word
        """
        if self.name == None or self.name == "":
            self.add_error('name', 'That URL is not valid.')
            return False

        if self.name.lower() in reserved_names:
            self.add_error('name', 'That URL is reserved.')
            return False

        if len(self.name) > 0 and len(self.name) > 25:
            self.add_error('name', 'URLs can be 1 to 25 characters long.')
            return False

        if re.search("[^a-zA-Z0-9\-]", self.name):
            self.add_error(
                'name',
                'Username can only contain letters, numbers, and dashes.')
            return False

        existing_shake = shake.Shake.get("name = %s and deleted <> 1",
                                         self.name)
        if existing_shake and existing_shake.id != self.id:
            self.add_error('name', 'That URL is already taken.')
            return False

        return True

    def subscribers(self, page=None):
        sql = """SELECT user.* FROM subscription
                   JOIN user ON user.id = subscription.user_id AND user.deleted = 0
                   WHERE shake_id = %s
                       AND subscription.deleted = 0
                   ORDER BY subscription.id """
        if page > 0:
            limit_start = (page - 1) * 20
            sql = "%s LIMIT %s, %s" % (sql, limit_start, 20)
        return user.User.object_query(sql, self.id)

    def subscriber_count(self):
        sql = """SELECT count(*) AS subscriber_count FROM subscription
                   JOIN user ON user.id = subscription.user_id AND user.deleted = 0
                   WHERE shake_id = %s
                       AND subscription.deleted = 0"""
        count = user.User.query(sql, self.id)
        return int(count[0]['subscriber_count'])

    def sharedfiles_count(self):
        return shakesharedfile.Shakesharedfile.where_count(
            "shake_id=%s and deleted = 0", self.id)

    def sharedfiles(self, page=1, per_page=10):
        """
        Shared files, paginated.
        """
        limit_start = (page - 1) * per_page
        sql = """SELECT sharedfile.* FROM sharedfile, shakesharedfile
                 WHERE shakesharedfile.shake_id = %s and shakesharedfile.sharedfile_id = sharedfile.id and
                       shakesharedfile.deleted = 0 and sharedfile.deleted = 0
                 ORDER BY shakesharedfile.sharedfile_id desc limit %s, %s"""
        return sharedfile.Sharedfile.object_query(sql, self.id,
                                                  int(limit_start), per_page)

    def sharedfiles_paginated(self, per_page=10, since_id=None, max_id=None):
        """
        Pulls a shake's timeline, can key off and go backwards (max_id) and forwards (since_id)
        in time to pull the per_page amount of posts.
        """
        constraint_sql = ""
        order = "desc"
        if max_id:
            constraint_sql = "AND shakesharedfile.sharedfile_id < %s" % (
                int(max_id))
        elif since_id:
            order = "asc"
            constraint_sql = "AND shakesharedfile.sharedfile_id > %s" % (
                int(since_id))

        sql = """SELECT sharedfile.* FROM sharedfile, shakesharedfile
                 WHERE shakesharedfile.shake_id = %s and shakesharedfile.sharedfile_id = sharedfile.id and
                 shakesharedfile.deleted = 0 and sharedfile.deleted = 0
                 %s
                 ORDER BY shakesharedfile.sharedfile_id %s limit %s, %s""" % (
            int(self.id), constraint_sql, order, 0, int(per_page))
        results = sharedfile.Sharedfile.object_query(sql)

        if order == "asc":
            results.reverse()

        return results

    def add_manager(self, user_to_add=None):
        if options.readonly:
            return False

        if not user_to_add:
            return False

        if user_to_add.id == self.user_id:
            return False

        #was this user a previous manager?
        existing_manager = shakemanager.ShakeManager.get(
            "user_id = %s AND shake_id = %s", user_to_add.id, self.id)
        if existing_manager:
            existing_manager.deleted = 0
            existing_manager.save()
        else:
            new_manager = shakemanager.ShakeManager(shake_id=self.id,
                                                    user_id=user_to_add.id)
            new_manager.save()
        return True

    def remove_manager(self, user_to_remove=None):
        if options.readonly:
            return False

        if not user_to_remove:
            return False

        if user_to_remove.id == self.user_id:
            return False

        existing_manager = shakemanager.ShakeManager.get(
            "user_id = %s AND shake_id = %s", user_to_remove.id, self.id)
        if existing_manager:
            existing_manager.deleted = 1
            existing_manager.save()
            return True
        else:
            return False

    def managers(self):
        sql = """SELECT user.* FROM shake_manager
                 JOIN user ON user.id = shake_manager.user_id AND user.deleted = 0
                 WHERE shake_manager.shake_id = %s
                    AND shake_manager.deleted = 0
                 ORDER BY user.id""" % (self.id)
        return user.User.object_query(sql)

    def is_owner(self, user):
        """
        A convenience method that accepts a None value or a user
        object and returns True or False if the user owns the shake.
        """
        if not user:
            return False
        if self.user_id != user.id:
            return False
        return True

    def can_edit(self, user):
        """
        Determines whether or not a user can edit a file.  Currently
        only the owner can edit.  Accepts None value.
        """
        if options.readonly:
            return False

        if not user:
            return False
        return self.is_owner(user)

    def set_page_image(self, file_path=None, sha1_value=None):
        thumb_cstr = cStringIO.StringIO()
        image_cstr = cStringIO.StringIO()

        if not file_path or not sha1_value:
            return False

        #generate smaller versions
        if not transform_to_square_thumbnail(file_path, 48 * 2, thumb_cstr):
            return False

        if not transform_to_square_thumbnail(file_path, 284 * 2, image_cstr):
            return False

        bucket = S3Bucket()

        try:
            #save thumbnail
            k = Key(bucket)
            k.key = "account/%s/shake_%s_small.jpg" % (self.user_id, self.name)
            k.set_metadata('Content-Type', 'image/jpeg')
            k.set_metadata('Cache-Control', 'max-age=86400')
            k.set_contents_from_string(thumb_cstr.getvalue())
            k.set_acl('public-read')

            #save small
            k = Key(bucket)
            k.key = "account/%s/shake_%s.jpg" % (self.user_id, self.name)
            k.set_metadata('Content-Type', 'image/jpeg')
            k.set_metadata('Cache-Control', 'max-age=86400')
            k.set_contents_from_string(image_cstr.getvalue())
            k.set_acl('public-read')

            self.image = 1
            self.save()
        except Exception as e:
            return False

        return True

    def feed_date(self):
        """
        Returns a date formatted to be included in feeds
        e.g., Tue, 12 Apr 2005 13:59:56 EST
        """
        return self.created_at.strftime("%a, %d %b %Y %H:%M:%S %Z")

    @classmethod
    def featured_shakes(self, limit=3):
        """
        Return a randomly sorted list of featured shakes.
        """
        return self.where(
            "type = 'group' and featured = 1 order by rand() limit %s", limit)

    @classmethod
    def for_category(self, category):
        """
        Return a randomly sorted list of recommended shakes for a category.
        """
        return self.where(
            "type = 'group' and shake_category_id = %s order by name",
            category.id)
Ejemplo n.º 30
0
class Invitation(Model):
    # Id of user who sent the invitation out
    user_id = Property()

    # Id of user who used the invitation
    claimed_by_user_id = Property()

    # somewhat secret key to point to invitation
    invitation_key = Property()

    # email address that invitation key was sent to
    email_address = Property()

    # name of person who invitation was meant for
    name = Property()

    created_at = Property()
    claimed_at = Property()

    def save(self, *args, **kwargs):
        if options.readonly:
            self.add_error('_', 'Site is read-only.')
            return False

        self._set_dates()
        return super(Invitation, self).save(*args, **kwargs)

    def _set_dates(self):
        """
        Sets the created_at and updated_at fields. This should be something
        a subclass of Property that takes care of this during the save cycle.
        """
        if self.id is None or self.created_at is None:
            self.created_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    @classmethod
    def create_for_email(self, email, user_id):
        """
        Creates an invitation for an email address.
        """
        h = hashlib.sha1()
        h.update("%s" % (time.time()))
        h.update("%s" % (email))
        invitation_key = h.hexdigest()
        sending_user = user.User.get('id = %s', user_id)
        invitation = Invitation(user_id=user_id,
                                invitation_key=invitation_key,
                                email_address=email,
                                claimed_by_user_id=0)
        invitation.save()
        if not options.debug:
            text_body = """Hi there. A user on MLTSHP named %s has sent you this invitation to join the site.

You can claim it at this URL:

http://mltshp.com/create-account?key=%s

Be sure to check out the incoming page for fresh files being uploaded, when you find someone you want to keep track of, click the "follow" button on their profile to see their files when you first sign in.

We're adding features and making updates daily so please check back often.

Once you have your account set up, check out:
http://mltshp.com/tools/plugins (browser plugins for saving images)
http://mltshp.com/tools/twitter (connecting your phone's Twitter app to use MLTSHP instead of Twitpic or yFrog)
http://twitter.com/mltshp (our twitter account)
http://mltshp.tumblr.com/ (our blog)

- MLTSHP""" % (sending_user.name, invitation_key)
            html_body = """<p>Hi there. A user on MLTSHP named <a href="http://mltshp.com/user/%s">%s</a> has sent you this invitation to join the site.</p>

<p>You can claim it at this URL:</p>

<p><a href="http://mltshp.com/create-account?key=%s">http://mltshp.com/create-account?key=%s</a></p>

<p>Be sure to check out the <a href="http://mltshp.com/incoming">incoming</a> page for fresh files being uploaded, when you find someone you want to keep track of, click the "follow" button on their profile to see their files when you first sign in.</p>

<p>We&#39;re adding features and making updates daily so please check back often.</p>

<p>Once you have your account set up, check out:</p>
<p>
<a href="http://mltshp.com/tools/plugins">http://mltshp.com/tools/plugins</a> (browser plugins for saving images)<br>
<a href="http://mltshp.com/tools/twitter">http://mltshp.com/tools/twitter</a> (connecting your phone's Twitter app to use MLTSHP instead of Twitpic or yFrog)<br>
<a href="http://twitter.com/mltshp">http://twitter.com/mltshp</a> (our twitter account)<br>
<a href="http://mltshp.tumblr.com/">http://mltshp.tumblr.com/</a> (our weblog)
</p>
<p>
- MLTSHP
</p>""" % (sending_user.name, sending_user.name, invitation_key,
            invitation_key)

            pm = postmark.PMMail(api_key=options.postmark_api_key,
                                 sender="*****@*****.**",
                                 to=email,
                                 subject="An Invitation To MLTSHP",
                                 text_body=text_body,
                                 html_body=html_body)
            pm.send()
        return invitation

    @classmethod
    def by_email_address(self, email):
        """
        Returns invitation where email address matches and is not claimed.  We use a 
        where query here since we don't enforce uniqueness on email address. Just returns 1st.
        """
        if not email:
            return None
        invitations = self.where(
            "email_address = %s and claimed_by_user_id = 0", email)
        try:
            return invitations[0]
        except IndexError:
            return None

    @classmethod
    def by_invitation_key(self, key):
        """
        Returns invitation where email address matches and is not claimed.  We use a 
        where query here since we don't enforce uniqueness on key. Just returns 1st.
        """
        if not key:
            return None
        invitations = self.where(
            "invitation_key = %s and claimed_by_user_id = 0", key)
        try:
            return invitations[0]
        except IndexError:
            return None

    @classmethod
    def by_user(self, user_):
        """
        Returns all invitations sent out by user.
        """
        return self.where("user_id = %s", user_.id)