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)]
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()
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()")
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)
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()
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)
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
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)
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)
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
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")
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()
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()
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
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
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")
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
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")
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()
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
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()
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
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'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)
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('"', '"', 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
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()
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…" % 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
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']
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)
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)
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'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)