예제 #1
0
파일: models.py 프로젝트: vanpav/top-24.su
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])
예제 #2
0
파일: models.py 프로젝트: vanpav/top-24.su
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()
예제 #3
0
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)
예제 #4
0
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
예제 #5
0
파일: models.py 프로젝트: vanpav/top-24.su
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)
예제 #6
0
파일: models.py 프로젝트: vanpav/top-24.su
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()
예제 #7
0
파일: models.py 프로젝트: vanpav/top-24.su
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)
예제 #8
0
파일: models.py 프로젝트: vanpav/top-24.su
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()
예제 #9
0
파일: models.py 프로젝트: vanpav/top-24.su
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()