class ApishopOffer(TempDBMixin, GetOrCreateMixin, db.Document): id = db.IntField(primary_key=True) available = db.BooleanField() articul = db.StringField() price = db.DictField(default=dict(ru=None, by=None, kz=None)) commissions = db.DictField(default=dict(ru=None, by=None, kz=None)) category_id = db.IntField() name = db.StringField(max_length=255) model = db.StringField(max_length=255) vendor = db.StringField(max_length=100) pictures = db.ListField(db.StringField(max_length=255), default=[]) description = db.StringField() variants = db.ListField(db.DictField()) store_count = db.IntField() enabled_to_copy = db.BooleanField(default=True) meta = {'indexes': ['category_id']} def __unicode__(self): return '%s: %s' % (self.id, self.name) def get_picture(self): if len(self.pictures): return self.pictures[0] @property def prepare_to_copy(self): return dict(aid=self.id, articul=self.articul, available=self.available, price=self.price, commissions=self.commissions, name=self.name, model=self.model, vendor=self.vendor, pictures=[{ 'url': picture } for picture in self.pictures], description=self.description, stats=dict(store_count=self.store_count), variants=[{ 'store_count': variant.get('store_count'), 'aid': variant.get('id'), 'name': variant.get('name') } for variant in self.variants])
class Review(db.Document): offer = db.ReferenceField('Offer') fullname = db.StringField(max_length=200) email = db.StringField(max_length=200) text = db.StringField() rating = db.IntField(default=0) is_moderated = db.BooleanField(default=False) is_viewed = db.BooleanField(default=False) created_at = db.DateTimeField(default=datetime.now) meta = { 'ordering': ['-created_at'] } def toggle_moderate(self): self.update(set__is_moderated=not self.is_moderated) self.reload() def set_viewed(self): if not self.is_viewed: self.update(set__is_viewed=True) self.reload()
class User(db.Document, UserMixin): email = db.StringField(max_length=255, unique=True) password = db.StringField(max_length=255, required=True) name = db.StringField(max_length=255) active = db.BooleanField(default=True) roles = db.ListField(db.ReferenceField(Role), default=[]) registered_at = db.DateTimeField() def __unicode__(self): return 'User %s' % self.email @property def is_admin(self): return self.has_role('admin') def save(self, *args, **kwargs): self.registered_at = datetime.now() super(User, self).save(*args, **kwargs)
class Subscriber(IntIDMixin, db.Document): name = db.StringField(max_length=200, required=True) email = db.StringField(max_length=200, required=True) subscribed_at = db.DateTimeField(default=datetime.now()) is_active = db.BooleanField(default=True) def __unicode__(self): return '<Subscriber {}: {}>'.format(self.id, self.email) def mark_subscribed(self): userinfo = session.get('userinfo', None) if not userinfo: session['userinfo'] = dict(fullname=self.name, email=self.email) else: if 'fullname' not in userinfo: userinfo['fullname'] = self.name if 'email' not in userinfo: userinfo['email'] = self.email session['is_subscribed'] = True
class Banner(db.Document): banner_type = db.StringField(choices=type_choices, default=u'small', max_length=100, verbose_name=u'Тип баннера') bg_color = db.StringField(default='#333333', max_length=7, verbose_name=u'Цвет фона') is_enabled = db.BooleanField(default=True) meta = { 'allow_inheritance': True } @classmethod def get_class_by_group(cls, group): return eval('{}Banner'.format(group.capitalize())) @classmethod @cache.memoize(60) def get_banners_grouped(cls, only_enabled=True): filters = {} if only_enabled: filters['is_enabled'] = True banners = cls.objects(**filters) grouped_banners = {} for banner in banners: if banner.banner_type in grouped_banners: grouped_banners[banner.banner_type].append(banner) else: grouped_banners[banner.banner_type] = [banner] return grouped_banners @classmethod def get_banners(cls, group=None, only_enabled=True): banners = cls.get_banners_grouped(only_enabled) grouped_banners = {} for g, bs in banners.items(): path = request.path new_bs = [] for b in bs: if path != b.link_to: new_bs.append(b) max = max_banners_items.get('MAX_{}_BANNERS'.format(g.upper())) random.shuffle(new_bs) if len(new_bs) > max: new_bs = new_bs[:max] grouped_banners[g] = new_bs if group: return grouped_banners.get(group, []) return grouped_banners @property def get_link_to(self): if not self.link_to: return url_for('site.index') return '/{}/'.format(self.link_to.strip('/')) def generate_image_name(self): return str(self.id) def upload_image(self, file): size = self.image_size or (300, 300) original = upload_file(file, self.generate_image_name(), 'banners') image = create_offer_image(original, format='png', width=size[0], height=size[1], suffix='b', fill=0, quality=100) if os.path.exists(os.path.join(current_app.config.get('MEDIA_DIR'), original)): os.remove(os.path.join(current_app.config.get('MEDIA_DIR'), original)) return image def save(self, *args, **kwargs): cache.delete_memoized(Banner.get_banners_grouped) super(Banner, self).save(*args, **kwargs)
class Cart(db.Document): offers = db.ListField(db.EmbeddedDocumentField('CartOffer'), default=[]) total = db.EmbeddedDocumentField('CartTotal') ordered = db.BooleanField(default=False) @classmethod def get_or_create(cls): cart_id = session.get('cart_id', None) if cart_id: cart = cls.objects(id=cart_id).first() else: cart = None if not cart: cart = cls() cart.save() session['cart_id'] = cart.id return cart @property def is_empty(self): return len(self.offers) == 0 def get_offer_ids(self): return [offer.offer.id for offer in self.offers] def get_offer(self, offer_id): return Offer.objects(id=offer_id).first() def add_offer(self, offer_id, quantity=1, variant=None): offer = self.get_offer(offer_id) try: quantity = int(quantity) except (TypeError, ValueError): quantity = 1 if offer and offer.is_in_stock: variant = offer.get_variant(variant) if variant: variant = str(variant.aid) for i, cart_offer in enumerate(self.offers): if offer.id == cart_offer.offer.id and variant and cart_offer.variant == variant: new_quantity = cart_offer.quantity + quantity self.offers[i] = CartOffer(offer=offer, quantity=new_quantity, variant=variant) self.save() break elif not variant and offer.id == cart_offer.offer.id: new_quantity = cart_offer.quantity + quantity self.offers[i] = CartOffer(offer=offer, quantity=new_quantity) self.save() break else: self.update(push__offers=CartOffer(offer=offer, quantity=quantity, variant=variant)) self.reload() offer.set_add_to_cart() self.calculate_total() def remove_offer(self, offer_id, variant_id=None): try: offer_id = int(offer_id) except (TypeError, ValueError) as e: return variant_id = str(variant_id) if variant_id != '' else None for idx, offer in enumerate(self.offers): if variant_id: if offer_id == offer.offer.id and variant_id == offer.variant: self.offers.pop(idx) break else: if offer_id == offer.offer.id: self.offers.pop(idx) break self.save() self.calculate_total() def calculate_total(self): total_price = [] total_quantity = [] for offer in self.offers: total_price.append(offer.offer.get_price() * offer.quantity) total_quantity.append(offer.quantity) self.update(set__total=CartTotal(cost=sum(total_price), count=sum(total_quantity))) def prepare_offers(self): return [dict(articul=offer.offer.articul, aid=offer.offer.aid, quantity=offer.quantity, variant=offer.variant, price=offer.offer.get_price()) for offer in self.offers] def send_order(self, order_form): from web.site import send_order order_id = send_order(self.prepare_offers(), order_form) self.clear_cart() return order_id def clear_cart(self): self.update(set__offers=[]) self.reload() self.calculate_total()
class Offer(DispatcherMixin, IntIDMixin, PathMixin, BreadcrumbsMixin, db.Document): name = db.StringField(max_length=255, verbose_name=u'Название', required=True) model = db.StringField(max_length=255, verbose_name=u'Модель') aid = db.IntField(verbose_name=u'ID apishops') articul = db.StringField(max_length=50, verbose_name=u'Артикул') available = db.BooleanField() price = db.EmbeddedDocumentField('OfferPrices', verbose_name=u'Цены') commissions = db.EmbeddedDocumentField('OfferPrices', verbose_name=u'Коммиссии') vendor = db.ReferenceField('Vendor', verbose_name=u'Производитель') parent = db.ReferenceField('Category', verbose_name=u'Категория') metas = db.EmbeddedDocumentField('Metas') stats = db.EmbeddedDocumentField('OfferStats') variants = db.ListField(db.EmbeddedDocumentField('OfferVariant')) pictures = db.ListField(db.EmbeddedDocumentField('OfferPicture')) special = db.ReferenceField('OfferSpecial') short_description = db.StringField(verbose_name=u'Короткое описание') description = db.StringField(verbose_name=u'Описание') canonical = db.ReferenceField('Offer', default=None, verbose_name=u'Каноникал') meta = { 'indexes': [{'fields': ['$name', "$description"], 'default_language': 'russian', 'weights': {'name': 10, 'description': 2} }, 'path', 'parent', 'articul', 'aid', 'price.ru', 'stats.popularity', ['available', 'price.ru'], ['available', 'stats.popularity']] } def __unicode__(self): return self.name def __repr__(self): return u'%s(%s)' % (self.__class__.__name__, self.id) @cache.memoize(60*60*24*7) def get_canonical(self): if not self.canonical or self.canonical == self: return None url_root = request.url_root return urlparse.urljoin(url_root, url_for('site.dispatcher', path=self.canonical.path)) @classmethod def populate(cls, copied_offer, category): offer = cls.objects(aid=copied_offer.id, articul=copied_offer.articul).first() if not offer: offer = cls.objects(aid=copied_offer.id).first() offer_info = deepcopy(copied_offer.prepare_to_copy) if offer: store_count = offer_info.get('stats').get('store_count', None) available = offer_info.get('available', None) price = offer_info.get('price', None) commissions = offer_info.get('commissions', None) variants = offer_info.get('variants', None) updates = {} price_change = False if copied_offer.articul != offer.articul: updates['set__articul'] = copied_offer.articul if store_count != offer.stats.store_count: updates['set__stats__store_count'] = store_count if available != offer.available: updates['set__available'] = available if price: for key in ('ru', 'by', 'kz'): if float(price.get(key)) != float(offer.get_price(key)): if key == 'ru': price_change = True updates['set__price__{}'.format(key)] = price.get(key) if commissions: for key in ('ru', 'by', 'kz'): if float(commissions.get(key)) != float(offer.get_commission(key)): updates['set__commissions__{}'.format(key)] = commissions.get(key) if variants: varts = [] for variant in variants: varts.append(OfferVariant(**variant)) updates['set__variants'] = varts if len(updates.keys()): if price_change: from modules.apishop.models import ApishopPriceChange if offer.special and offer.special.type == 'real': old_price = float(offer.special.prices.ru) else: old_price = float(offer.get_price('ru')) new_price = float(updates['set__price__ru']) if old_price != new_price: change = ApishopPriceChange(oid=offer.id, name=offer.name, old_price=old_price, new_price=new_price) change.save() updates['set__updated_at'] = datetime.now() offer.update(**updates) offer.reload() if price_change and offer.get_special is not None: current_special = offer.get_special new = current_special.create_from_self() new.populate_price(offer) new.save() offer.update(set__special=new) current_special.delete() else: vendor_name = offer_info.pop('vendor', None) if vendor_name: vendor = Vendor.get_or_create_by_name(vendor_name) else: vendor = None offer_info['vendor'] = vendor offer_info['parent'] = category offer = cls(**offer_info) offer.save() task = upload_offer_pictures.apply_async([offer]) @cache.memoize(60*60) def get_delivery_price(self, region_id=None): if not region_id: region_id = 53 region = Region.objects(id=region_id).first() if region: return dict(id=region.id, name=region.name, deliveries=[(d.method, d.price) for d in region.deliveries]) return None @staticmethod def sub_text(match): id = match.group('id') offer = Offer.objects(id=id).only('path').first() if not offer: return '' return url_for('site.dispatcher', path=offer.path) @property def get_description(self): comp = re.compile(r'%%\s*link_to_offer\s+(?P<id>[0-9]+)\s*%%', re.IGNORECASE) text = comp.sub(self.sub_text, self.description) return u'{}'.format(text) @property def is_in_favorites(self): favorites = session.get('favorites', []) return str(str(self.id) in favorites).lower() @property def generate_picture_name(self): uniq = str(uuid.uuid4())[:8] return '_'.join([self.slug, uniq]) def create_pictures_set(self, original): big = create_offer_image(original, quality=100) medium = create_offer_image(original, width=250, height=200, suffix='med') small = create_offer_image(original, width=60, height=60, suffix='sml') return dict(original=original, big=big, medium=medium, small=small) def get_pictures(self, for_download=False, typ=None): if for_download: return self.pictures if hasattr(self, 'pictures') else [] pictures = [] if self.pictures: for picture in self.pictures: if typ and getattr(picture, typ, None) is not None: url = url_for('media', filename=getattr(picture, typ)) elif not typ and getattr(picture, 'big', None) is not None: url = url_for('media', filename=getattr(picture, 'big')) else: url = url_for('media', filename=picture.original) if picture.original else picture.url pictures.append(url) else: if not typ: typ = 'big' pictures = [url_for('static', filename='img/nophoto_{}.svg'.format(typ))] return pictures @cached_property @cache.memoize(3600) def parent_cached(self): return (self.parent.name, self.parent.path) def get_variant(self, aid): if self.variants: try: aid = int(aid) except (TypeError, ValueError): pass for variant in self.variants: if variant.aid == aid: return variant else: return self.variants[0] return None def get_reviews(self): return Review.objects(offer=self.id, is_moderated=True) @cached_property def get_title(self): separator = current_app.config.get('DEFAULT_TITLE_SEPARATOR', ' | ') return separator.join([self.name, self.parent.get_title]) @property def is_in_stock(self): return self.available @property def get_special(self): return self.special or None @property def get_oldprice(self): if self.special: return smart_round(self.special.prices.ru) return None @property def get_timer(self): if self.special: return self.special.timer return None @classmethod def get_special_offers(cls): import random special_offers = cls.objects(special__ne=None).order_by('-stats.popularity') special_offers = [offer for offer in special_offers if offer.special.is_active] random.shuffle(special_offers) return special_offers @cache.memoize(3600) def get_picture(self, typ=None, absolute=False): pictures = self.get_pictures(typ=typ) if len(pictures): url = pictures[0] else: url = url_for('static', filename='img/nophoto.svg') if absolute: url = urlparse.urljoin(request.url_root, url) return url @cached_property def get_absolute_picture(self): pic = self.get_picture() if not pic: return None return ''.join([request.url_root.strip('/'), pic]) @cached_property def get_canonical_url(self): return ''.join([request.url_root.strip('/'), url_for('site.dispatcher', path=self.path)]) @cache.memoize(3600) def get_breadcrumbs(self): paths = self._split_path() breadcrumbs = [] objs = self.parent.__class__.objects(path__in=paths[:-1])\ .only('name', 'path')\ .order_by('path') for obj in objs: breadcrumbs.append((obj.name, obj.path)) return breadcrumbs def set_visit(self): cache.delete_memoized(self.get_visited) visited_offers = session.get('visited_offers', []) if self.id not in visited_offers: self.update(inc__stats__views=1) self.reload() visited_offers.insert(0, self.id) self.calculate_popularity() else: visited_offers.remove(self.id) visited_offers.insert(0, self.id) session['visited_offers'] = visited_offers def set_add_to_cart(self): added_to_cart = session.get('added_to_cart', []) if self.id not in added_to_cart: self.update(inc__stats__add_to_cart=1) self.reload() added_to_cart.append(self.id) self.calculate_popularity() self.reload() session['added_to_cart'] = added_to_cart def calculate_popularity(self): popularity = (self.stats.views * 1 + self.stats.add_to_cart * 2 + self.stats.orders * 3) / 3 self.update(set__stats__popularity=popularity) @classmethod @cache.memoize(60*5) def get_visited(cls): visited_offers = session.get('visited_offers', []) if len(visited_offers): visited = list(cls.objects(id__in=visited_offers[:15]) .only('id', 'name', 'price', 'pictures', 'path')) visited.sort(key=lambda k: visited_offers.index(k.id)) return visited return None @classmethod def get_popular(cls): return cls.objects(available=True).order_by('-stats.popularity') @cache.memoize(60*60) def get_random_ids(self, offer_id): max_items = 12 all_ids = sorted(Offer.objects(available=True, id__ne=offer_id).distinct('id')) length = len(all_ids) ids = [] if length: try: ids = random.sample(all_ids, max_items) except ValueError: ids = random.sample(all_ids, length) return ids def get_related(self): return self.__class__.objects(id__in=self.get_random_ids(self.id)) def remove_picture(self, idx): pictures = self.pictures picture = pictures.pop(idx) for typ in ('original', 'small', 'medium', 'big'): if hasattr(picture, typ): path = getattr(picture, typ, None) if path: delete_file_by_path(os.path.join(current_app.config['MEDIA_DIR'], path)) self.update(set__pictures=pictures) cache.delete_memoized(self.get_picture) def get_price(self, key='ru'): return smart_round(getattr(self.price, key)) def get_commission(self, key='ru'): return smart_round(getattr(self.commissions, key)) def save(self, *args, **kwargs): cache.delete_memoized(self.get_picture) cache.delete_memoized(self.get_visited) cache.delete_memoized(self.get_canonical) super(Offer, self).save(*args, **kwargs)
class Category(DispatcherMixin, PositionMixin, IntIDMixin, PathMixin, BreadcrumbsMixin, db.Document): name = db.StringField(max_length=255, verbose_name=u'Название') is_active = db.BooleanField(default=True, verbose_name=u'Включена') parent = db.ReferenceField('Category', default=None, verbose_name=u'Родитель') description = db.StringField(verbose_name=u'Описание') stats = db.EmbeddedDocumentField('CategoryStats') metas = db.EmbeddedDocumentField('Metas') meta = { 'ordering': ['+position'], 'indexes': [{'fields': ['$name', "$description"], 'default_language': 'russian', 'weights': {'name': 10, 'description': 2} }, 'path', 'parent', 'position'] } def __unicode__(self): return u'%s' % self.name def __repr__(self): return u'%s(%s)' % (self.__class__.__name__, self.id) def get_childrens(self): return self.__class__.objects(parent=self) def get_childs(self): return self.__class__.objects(__raw__={'path': {'$regex': '^{0}' .format(self.path)}}).order_by('path') def delete(self, *args, **kwargs): childrens = self.get_childrens() for child in childrens: child.delete() cache.delete_memoized(self.get_tree) super(Category, self).delete(*args, **kwargs) def save(self, *args, **kwargs): self.validate_position(kwargs.get('parent', self.parent)) cache.delete_memoized(self.get_tree) super(Category, self).save(*args, **kwargs) @cache.memoize(3600) def get_tree_from(self): paths = self._split_path() root_category = self.__class__.objects.get(path=paths[0]) tree = self.__class__.get_tree(parent=None, paths=paths) return tree def get_breadcrumbs(self): paths = self._split_path() breadcrumbs = [] for path in paths[:-1]: obj = self.__class__.objects.get(path=path) breadcrumbs.append((obj.name, obj.path)) return breadcrumbs @cache.memoize(60*60*24*7) def get_category_root_url(self): if not self.parent: return None root = self.__class__.objects(path=self.path.split('/')[0]).first() return urlparse.urljoin(request.url_root, url_for('site.dispatcher', path=root.path)) @cache.memoize(60*60*24*7) def get_root(self): if not self.parent: return self paths = self._split_path() root = self.__class__.objects(path=paths[0]).first() return root @cached_property def get_title(self): if self.metas.title: return self.metas.title names = [self.name] parent = self.parent while parent: names.append(parent.name) parent = parent.parent separator = current_app.config.get('DEFAULT_TITLE_SEPARATOR', ' | ') return separator.join(names) @property def get_offers_count(self): count = self.stats.items if self.stats else 0 return count @property def depth(self): return len(self.path.split('/')) - 1 @classmethod @cache.memoize(3600) def get_tree(cls, parent=None, paths=None): objects = cls.objects(parent=parent) branch = [] if not paths: for obj in objects: childs = obj.get_childs() if childs.count() > 1: branch.append([obj, obj.__class__.get_tree(parent=obj)]) else: branch.append(obj) else: for obj in objects: if obj.path in paths: childs = obj.get_childs() if childs.count() > 1: branch.append([obj, obj.__class__.get_tree(parent=obj, paths=paths)]) else: branch.append(obj) else: branch.append(obj) return branch def save(self, *args, **kwargs): cache.delete_memoized(self.__class__.get_tree) cache.delete_memoized(self.get_tree_from) cache.delete_memoized(self.get_category_root_url) cache.delete_memoized(self.get_root) super(Category, self).save(*args, **kwargs) @classmethod def post_save(cls, sender, document, **kwargs): childs = cls.objects(__raw__={'path': {'$regex': '^{0}' .format(document.old_path)}}).order_by('path') if len(childs) > 1 and document.old_path != document.path: for child in childs: child.save()
class OfferSpecial(db.Document): is_active = db.BooleanField(default=False) type = db.StringField(choices=(('real', u'Понизить цену'), ('fake', u'Повысить цену')), default='real', verbose_name=u'Тип акции') price_type = db.StringField(choices=(('percent', u'На проценты'), ('new', u'На сумму')), default='percent') price_value = db.IntField(required=True) timer_type = db.StringField(choices=(('date', u'До даты'), ('time', u'По времени')), default='date') timer_settings = db.DictField(required=True) created_at = db.DateTimeField() prices = db.EmbeddedDocumentField('OfferPrices') def __unicode__(self): return str(self.id) @classmethod def create_or_update(cls, offer, form): special = offer.get_special or cls() form.populate_obj(special) special.populate_price(offer) special.set_created_at() special.save() offer.update(set__special=special) return special def create_from_self(self): offer = Offer.objects(special=self).first() if offer: data = deepcopy(self._data) data.pop('id') data.pop('prices') new = self.__class__(**data) return new def set_created_at(self): self.is_active = True self.created_at = datetime.now() def populate_price(self, offer): if self.type == 'real': if not self.prices: self.prices = offer.price if self.price_type == 'percent': new_price = self.prices.ru * (1 - float(self.price_value) / 100) elif self.price_type == 'new': new_price = self.prices.ru - self.price_value offer.update(set__price__ru=new_price) offer.reload() else: if not self.prices: self.prices = offer.price if self.price_type == 'percent': new_price = self.prices.ru * (1 + float(self.price_value) / 100) elif self.price_type == 'new': new_price = self.prices.ru + self.price_value self.prices.ru = new_price def get_timer(self): if self.timer_type == 'date': date = datetime.strptime(self.timer_settings.get('timer_date'), "%d/%m/%Y").date() return date + timedelta(days=1) elif self.timer_type == 'time': now = datetime.now() delta = now - self.created_at days_step = int(self.timer_settings.get('timer_days', 1)) if self.timer_settings.get('timer_repeat') == 'on' and delta.days > days_step: full = delta.days - (delta.days % days_step) + days_step new_delta = timedelta(days=full) else: new_delta = timedelta(days=days_step) return (self.created_at + new_delta + timedelta(days=1)).date() @property def timer(self): return self.get_timer().strftime('%Y/%m/%d') @cached_property def is_over(self): now = datetime.now() is_over = now.date() >= self.get_timer() if is_over: offer = Offer.objects(special=self).first() if offer: self.remove(offer) else: self.update(set__is_active=False) return is_over def remove(self, offer): atomic = dict(set__special=None) if self.type == 'real': atomic['set__price'] = self.prices offer.update(**atomic) self.delete()