예제 #1
0
class ThreadedComment(MPTTModel, BaseCommentAbstractModel):

    # Copied from comments.Comment
    user = models.ForeignKey(User,
                             verbose_name=_('user'),
                             blank=True,
                             null=True,
                             related_name="%(class)s_comments")
    comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
    submit_date = models.DateTimeField(_('date/time submitted'), default=None)
    ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
    is_public = models.BooleanField(
        _('is public'),
        default=True,
        help_text=_('Uncheck this box to make the comment effectively '
                    'disappear from the site.'))
    is_removed = models.BooleanField(
        _('is removed'),
        default=False,
        help_text=_('Check this box if the comment is inappropriate. '
                    'A "This comment has been removed" message will '
                    'be displayed instead.'))
    objects = CommentManager()

    class Meta:
        ordering = (
            'tree_id',
            'submit_date',
        )

    def __unicode__(self):
        if self.is_removed:
            return u"a deleted comment"
        try:
            name = self.user.username
        except Exception:
            name = None
        return u"%s: %s..." % (name, self.comment[:50])

    def save(self, *args, **kwargs):
        if self.submit_date is None:
            self.submit_date = timezone.now()

        # Forbid more than 2 levels
        if self.parent_id is not None:
            while self.parent.get_level() > 0:
                self.parent = self.parent.parent

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

    def get_absolute_url(self, anchor_pattern="#comment-{id}"):
        url_base = self.get_content_object_url()
        return url_base + anchor_pattern.format(**self.__dict__)

    def get_reply_url(self, anchor_pattern="#reply-comment-{id}"):
        url_base = self.get_content_object_url()
        return url_base + anchor_pattern.format(**self.__dict__)

    def get_toplevel(self):
        if self.get_level() == 0:
            return self
        return self.parent.get_toplevel()

    # My stuff
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children')
    post_to_facebook = models.BooleanField(default=False)

    class MPTTMeta:
        # comments on one level will be ordered by date of creation
        order_insertion_by = ['submit_date']
예제 #2
0
class Page(MPTTModel):
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            verbose_name=u'Родительская страница')
    slug = models.SlugField(
        verbose_name=u'Slug',
        max_length=255,
        db_index=True,
        help_text=u'Внимание! Последующее редактирование поля slug невозможно!'
    )
    url_path = models.CharField(
        max_length=2048,
        db_index=True,
    )

    public = models.BooleanField(
        verbose_name=u'Опубликована?',
        default=False,
        db_index=True,
        help_text=
        u'Публиковать страницу могут только пользователи с правами публикации страниц'
    )
    create_date = models.DateTimeField(verbose_name=u"Дата создания",
                                       auto_now_add=True,
                                       db_index=True)

    class Meta:
        ordering = ['-create_date']
        permissions = (
            ("view_page", "Can view page"),
            ("public_page", "Can public page"),
        )

    def __unicode__(self):
        return self.slug

    def get_cur_lang_content(self):
        cur_language = get_language()
        try:
            content = Content.objects.get(page=self, lang=cur_language[:2])
        except Content.DoesNotExist:
            content = None
        return content

    def get_ancestors_titles(self):
        """
        return translated ancestors
        """
        ancestors = list(self.get_ancestors())
        lang = get_language()[:2]
        ad = {}
        for ancestor in ancestors:
            ad[ancestor.id] = ancestor
        contents = Content.objects.filter(page__in=ancestors,
                                          lang=lang).values(
                                              'page_id', 'title')
        for content in contents:
            ad[content['page_id']].title = content['title']
        return ancestors

    def save(self, *args, **kwargs):
        old = None
        if self.id:
            old = Page.objects.get(id=self.id)

        if old and self.slug != old.slug:
            self.slug = old.slug
        else:
            url_pathes = []
            if self.parent:
                for node in self.parent.get_ancestors():
                    url_pathes.append(node.slug)
                url_pathes.append(self.parent.slug)
                url_pathes.append(self.slug)
            else:
                url_pathes.append(self.slug)

            self.url_path = u'/'.join(url_pathes)

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

    def up(self):
        previous = self.get_previous_sibling()
        if previous:
            self.move_to(previous, position='left')

    def down(self):
        next = self.get_next_sibling()
        if next:
            self.move_to(next, position='right')
예제 #3
0
class Empresa(models.Model):

    ESTADO_CHOICES = (
        ('nuevo', 'Nuevo'),
        ('enproceso', 'En Proceso'),
        ('valido', 'Válido'),
        ('rechazado', 'Rechazado'),
    )

    TAMANO_CHOICES = (
        ('grandesempresas', 'Grandes Empresas'),
        ('pyme', 'PYME'),
        ('noaplica', 'No Aplica'),
    )

    FLOTA_CHOICES = (
        ('propia', 'Propia'),
        ('subcontratada', 'Subcontratada'),
        ('propiaysubcontratada', 'Propia y Subcontratada'),
    )

    # Información empresa
    rut = models.CharField('RUT', max_length=30)
    razon_social = models.CharField('Razón Social', max_length=100, blank=True)
    nombre = models.CharField('Nombre', max_length=100)
    email_corporativo = models.EmailField('Mail Corporativo')
    telefono_corporativo = models.CharField('Telefono Corporativo',
                                            max_length=50)
    giro_comercial = models.CharField('Giro Comercial',
                                      max_length=100,
                                      blank=True)
    representante = models.CharField('Representante Legal', max_length=100)
    direccion_corporativa = models.CharField('Direccion Corporativa',
                                             max_length=100,
                                             blank=True)
    contacto_corporativo = models.CharField('Contacto Corporativo',
                                            max_length=100,
                                            blank=True)
    covertura = models.ManyToManyField('Zona', blank=True)
    covertura_observacion = models.CharField('Covertura Observación',
                                             max_length=200,
                                             blank=True)
    comuna = TreeForeignKey('locationstree.Location', null=True)

    # Información tucarga
    estado = StatusField('Estado', choices_name='ESTADO_CHOICES')
    grupo_observacion = models.CharField('Tipo observación',
                                         max_length=20,
                                         blank=True)
    tamano = StatusField('Tamaño Empresa',
                         choices_name='TAMANO_CHOICES',
                         blank=True)
    actividad_economica = models.CharField('Actividad Economica',
                                           max_length=100,
                                           null=True,
                                           blank=True)
    fecha_registro = models.DateField('Fecha Registro', null=True, blank=True)
    ejecutivo = models.CharField('Ejecutivo',
                                 max_length=100,
                                 null=True,
                                 blank=True)
    grupo_piloto = models.BooleanField('Grupo Piloto', default=False)
    fecha_activacion = models.DateField('Fecha de activación',
                                        blank=True,
                                        null=True)
    tipo_carga = models.CharField('Tipo de carga',
                                  max_length=200,
                                  null=True,
                                  blank=True)
    flota = StatusField('Flota', choices_name='FLOTA_CHOICES', blank=True)
    viajes_mes = models.IntegerField('Viajes al mes', null=True, blank=True)
    viajes_mes_observacion = models.CharField('Viajes al mes observación',
                                              max_length=500,
                                              blank=True)
    sitio_web = models.CharField(max_length=200, null=True, blank=True)

    def __unicode__(self):
        return u'{}'.format(self.nombre)

    def get_absolute_url(self):
        return reverse('empresa_detail', kwargs={'pk': self.pk})
예제 #4
0
class Event(MPTTModel, BaseModel, SchemalessFieldMixin):

    jsonld_type = "Event/LinkedEvent"

    """
    eventStatus enumeration is based on http://schema.org/EventStatusType
    """
    SCHEDULED = 1
    CANCELLED = 2
    POSTPONED = 3
    RESCHEDULED = 4

    STATUSES = (
        (SCHEDULED, "EventScheduled"),
        (CANCELLED, "EventCancelled"),
        (POSTPONED, "EventPostponed"),
        (RESCHEDULED, "EventRescheduled"),
    )

    # Properties from schema.org/Thing
    url = models.URLField(_('Event home page'), blank=True)
    description = models.TextField(blank=True)

    # Properties from schema.org/CreativeWork
    date_published = models.DateTimeField(null=True, blank=True)
    # provider = models.ForeignKey(Organization, null=True, blank=True,
    #                             related_name='event_providers')

    # Properties from schema.org/Event
    event_status = models.SmallIntegerField(choices=STATUSES,
                                            default=SCHEDULED)
    location = models.ForeignKey(Place, null=True, blank=True)
    location_extra_info = models.CharField(max_length=400, null=True, blank=True)

    start_time = models.DateTimeField(null=True, db_index=True, blank=True)
    end_time = models.DateTimeField(null=True, db_index=True, blank=True)
    super_event = TreeForeignKey('self', null=True, blank=True,
                                 related_name='sub_event')

    # Custom fields not from schema.org
    target_group = models.CharField(max_length=255, null=True, blank=True)
    keywords = models.ManyToManyField(Category, null=True, blank=True)

    class Meta:
        verbose_name = _('event')
        verbose_name_plural = _('events')

    class MPTTMeta:
        parent_attr = 'super_event'

    def save(self, *args, **kwargs):
        if not self.id:
            self.created_time = BaseModel.now()
        self.last_modified_time = BaseModel.now()
        super(Event, self).save(*args, **kwargs)

    def same_as(self):
        return self.data_source.event_same_as(self.origin_id)

    def __str__(self):
        val = [self.name]
        dcount = self.get_descendant_count()
        if dcount > 0:
            val.append(u" (%d children)" % dcount)
        else:
            val.append(str(self.start_time))
        return u" ".join(val)
예제 #5
0
class Build(MPTTModel):
    """ A Build object organises the creation of new StockItem objects from other existing StockItem objects.

    Attributes:
        part: The part to be built (from component BOM items)
        reference: Build order reference (required, must be unique)
        title: Brief title describing the build (required)
        quantity: Number of units to be built
        parent: Reference to a Build object for which this Build is required
        sales_order: References to a SalesOrder object for which this Build is required (e.g. the output of this build will be used to fulfil a sales order)
        take_from: Location to take stock from to make this build (if blank, can take from anywhere)
        status: Build status code
        batch: Batch code transferred to build parts (optional)
        creation_date: Date the build was created (auto)
        target_date: Date the build will be overdue
        completion_date: Date the build was completed (or, if incomplete, the expected date of completion)
        link: External URL for extra information
        notes: Text notes
    """

    OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())

    class Meta:
        verbose_name = _("Build Order")
        verbose_name_plural = _("Build Orders")

    @staticmethod
    def filterByDate(queryset, min_date, max_date):
        """
        Filter by 'minimum and maximum date range'

        - Specified as min_date, max_date
        - Both must be specified for filter to be applied
        """

        date_fmt = '%Y-%m-%d'  # ISO format date string

        # Ensure that both dates are valid
        try:
            min_date = datetime.strptime(str(min_date), date_fmt).date()
            max_date = datetime.strptime(str(max_date), date_fmt).date()
        except (ValueError, TypeError):
            # Date processing error, return queryset unchanged
            return queryset

        # Order was completed within the specified range
        completed = Q(status=BuildStatus.COMPLETE) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)

        # Order target date falls witin specified range
        pending = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)

        # TODO - Construct a queryset for "overdue" orders

        queryset = queryset.filter(completed | pending)

        return queryset

    def __str__(self):

        prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")

        return f"{prefix}{self.reference}"

    def get_absolute_url(self):
        return reverse('build-detail', kwargs={'pk': self.id})
        
    reference = models.CharField(
        unique=True,
        max_length=64,
        blank=False,
        help_text=_('Build Order Reference'),
        verbose_name=_('Reference'),
        validators=[
            validate_build_order_reference
        ]
    )

    title = models.CharField(
        verbose_name=_('Description'),
        blank=False,
        max_length=100,
        help_text=_('Brief description of the build')
    )

    # TODO - Perhaps delete the build "tree"
    parent = TreeForeignKey(
        'self',
        on_delete=models.SET_NULL,
        blank=True, null=True,
        related_name='children',
        verbose_name=_('Parent Build'),
        help_text=_('BuildOrder to which this build is allocated'),
    )

    part = models.ForeignKey(
        'part.Part',
        verbose_name=_('Part'),
        on_delete=models.CASCADE,
        related_name='builds',
        limit_choices_to={
            'assembly': True,
            'active': True,
            'virtual': False,
        },
        help_text=_('Select part to build'),
    )

    sales_order = models.ForeignKey(
        'order.SalesOrder',
        verbose_name=_('Sales Order Reference'),
        on_delete=models.SET_NULL,
        related_name='builds',
        null=True, blank=True,
        help_text=_('SalesOrder to which this build is allocated')
    )
    
    take_from = models.ForeignKey(
        'stock.StockLocation',
        verbose_name=_('Source Location'),
        on_delete=models.SET_NULL,
        related_name='sourcing_builds',
        null=True, blank=True,
        help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)')
    )
    
    destination = models.ForeignKey(
        'stock.StockLocation',
        verbose_name=_('Destination Location'),
        on_delete=models.SET_NULL,
        related_name='incoming_builds',
        null=True, blank=True,
        help_text=_('Select location where the completed items will be stored'),
    )

    quantity = models.PositiveIntegerField(
        verbose_name=_('Build Quantity'),
        default=1,
        validators=[MinValueValidator(1)],
        help_text=_('Number of stock items to build')
    )

    completed = models.PositiveIntegerField(
        verbose_name=_('Completed items'),
        default=0,
        help_text=_('Number of stock items which have been completed')
    )

    status = models.PositiveIntegerField(
        verbose_name=_('Build Status'),
        default=BuildStatus.PENDING,
        choices=BuildStatus.items(),
        validators=[MinValueValidator(0)],
        help_text=_('Build status code')
    )
    
    batch = models.CharField(
        verbose_name=_('Batch Code'),
        max_length=100,
        blank=True,
        null=True,
        help_text=_('Batch code for this build output')
    )
    
    creation_date = models.DateField(auto_now_add=True, editable=False)
    
    target_date = models.DateField(
        null=True, blank=True,
        verbose_name=_('Target completion date'),
        help_text=_('Target date for build completion. Build will be overdue after this date.')
    )

    completion_date = models.DateField(null=True, blank=True)

    completed_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        blank=True, null=True,
        related_name='builds_completed'
    )
    
    link = InvenTree.fields.InvenTreeURLField(
        verbose_name=_('External Link'),
        blank=True, help_text=_('Link to external URL')
    )

    notes = MarkdownxField(
        verbose_name=_('Notes'),
        blank=True, help_text=_('Extra build notes')
    )

    @property
    def is_overdue(self):
        """
        Returns true if this build is "overdue":

        Makes use of the OVERDUE_FILTER to avoid code duplication
        """

        query = Build.objects.filter(pk=self.pk)
        query = query.filter(Build.OVERDUE_FILTER)

        return query.exists()
    
    @property
    def active(self):
        """
        Return True if this build is active
        """

        return self.status in BuildStatus.ACTIVE_CODES

    @property
    def bom_items(self):
        """
        Returns the BOM items for the part referenced by this BuildOrder
        """

        return self.part.bom_items.all().prefetch_related(
            'sub_part'
        )

    @property
    def remaining(self):
        """
        Return the number of outputs remaining to be completed.
        """

        return max(0, self.quantity - self.completed)

    @property
    def output_count(self):
        return self.build_outputs.count()

    def get_build_outputs(self, **kwargs):
        """
        Return a list of build outputs.

        kwargs:
            complete = (True / False) - If supplied, filter by completed status
            in_stock = (True / False) - If supplied, filter by 'in-stock' status
        """

        outputs = self.build_outputs.all()

        # Filter by 'in stock' status
        in_stock = kwargs.get('in_stock', None)

        if in_stock is not None:
            if in_stock:
                outputs = outputs.filter(StockModels.StockItem.IN_STOCK_FILTER)
            else:
                outputs = outputs.exclude(StockModels.StockItem.IN_STOCK_FILTER)

        # Filter by 'complete' status
        complete = kwargs.get('complete', None)

        if complete is not None:
            if complete:
                outputs = outputs.filter(is_building=False)
            else:
                outputs = outputs.filter(is_building=True)

        return outputs

    @property
    def complete_outputs(self):
        """
        Return all the "completed" build outputs
        """

        outputs = self.get_build_outputs(complete=True)

        # TODO - Ordering?

        return outputs

    @property
    def incomplete_outputs(self):
        """
        Return all the "incomplete" build outputs
        """

        outputs = self.get_build_outputs(complete=False)

        # TODO - Order by how "complete" they are?

        return outputs

    @property
    def incomplete_count(self):
        """
        Return the total number of "incomplete" outputs
        """

        quantity = 0

        for output in self.incomplete_outputs:
            quantity += output.quantity

        return quantity

    @classmethod
    def getNextBuildNumber(cls):
        """
        Try to predict the next Build Order reference:
        """

        if cls.objects.count() == 0:
            return None

        build = cls.objects.last()
        ref = build.reference

        if not ref:
            return None

        tries = set()

        while 1:
            new_ref = increment(ref)

            if new_ref in tries:
                # We are potentially stuck in a loop - simply return the original reference
                return ref

            if cls.objects.filter(reference=new_ref).exists():
                tries.add(new_ref)
                new_ref = increment(new_ref)
            else:
                break

        return new_ref

    @property
    def can_complete(self):
        """
        Returns True if this build can be "completed"

        - Must not have any outstanding build outputs
        - 'completed' value must meet (or exceed) the 'quantity' value
        """

        if self.incomplete_count > 0:
            return False

        if self.completed < self.quantity:
            return False

        # No issues!
        return True

    @transaction.atomic
    def complete_build(self, user):
        """
        Mark this build as complete
        """

        if not self.can_complete:
            return

        self.completion_date = datetime.now().date()
        self.completed_by = user
        self.status = BuildStatus.COMPLETE
        self.save()

        # Ensure that there are no longer any BuildItem objects
        # which point to thie Build Order
        self.allocated_stock.all().delete()

    @transaction.atomic
    def cancelBuild(self, user):
        """ Mark the Build as CANCELLED

        - Delete any pending BuildItem objects (but do not remove items from stock)
        - Set build status to CANCELLED
        - Save the Build object
        """

        for item in self.allocated_stock.all():
            item.delete()

        # Date of 'completion' is the date the build was cancelled
        self.completion_date = datetime.now().date()
        self.completed_by = user

        self.status = BuildStatus.CANCELLED
        self.save()

    def getAutoAllocations(self, output):
        """
        Return a list of StockItem objects which will be allocated
        using the 'AutoAllocate' function.

        For each item in the BOM for the attached Part,
        the following tests must *all* evaluate to True,
        for the part to be auto-allocated:

        - The sub_item in the BOM line must *not* be trackable
        - There is only a single stock item available (which has not already been allocated to this build)
        - The stock item has an availability greater than zero
        
        Returns:
            A list object containing the StockItem objects to be allocated (and the quantities).
            Each item in the list is a dict as follows:
            {
                'stock_item': stock_item,
                'quantity': stock_quantity,
            }
        """

        allocations = []

        """
        Iterate through each item in the BOM
        """

        for bom_item in self.bom_items:

            part = bom_item.sub_part

            # Skip any parts which are already fully allocated
            if self.isPartFullyAllocated(part, output):
                continue

            # How many parts are required to complete the output?
            required = self.unallocatedQuantity(part, output)

            # Grab a list of stock items which are available
            stock_items = self.availableStockItems(part, output)

            # Ensure that the available stock items are in the correct location
            if self.take_from is not None:
                # Filter for stock that is located downstream of the designated location
                stock_items = stock_items.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()])

            # Only one StockItem to choose from? Default to that one!
            if stock_items.count() == 1:
                stock_item = stock_items[0]

                # Double check that we have not already allocated this stock-item against this build
                build_items = BuildItem.objects.filter(
                    build=self,
                    stock_item=stock_item,
                    install_into=output
                )

                if len(build_items) > 0:
                    continue

                # How many items are actually available?
                if stock_item.quantity > 0:

                    # Only take as many as are available
                    if stock_item.quantity < required:
                        required = stock_item.quantity

                    allocation = {
                        'stock_item': stock_item,
                        'quantity': required,
                    }

                    allocations.append(allocation)

        return allocations

    @transaction.atomic
    def unallocateStock(self, output=None, part=None):
        """
        Deletes all stock allocations for this build.
        
        Args:
            output: Specify which build output to delete allocations (optional)

        """

        allocations = BuildItem.objects.filter(build=self.pk)

        if output:
            allocations = allocations.filter(install_into=output.pk)

        if part:
            allocations = allocations.filter(stock_item__part=part)

        # Remove all the allocations
        allocations.delete()

    @transaction.atomic
    def create_build_output(self, quantity, **kwargs):
        """
        Create a new build output against this BuildOrder.

        args:
            quantity: The quantity of the item to produce

        kwargs:
            batch: Override batch code
            serials: Serial numbers
            location: Override location
        """

        batch = kwargs.get('batch', self.batch)
        location = kwargs.get('location', self.destination)
        serials = kwargs.get('serials', None)

        """
        Determine if we can create a single output (with quantity > 0),
        or multiple outputs (with quantity = 1)
        """

        multiple = False

        # Serial numbers are provided? We need to split!
        if serials:
            multiple = True

        # BOM has trackable parts, so we must split!
        if self.part.has_trackable_parts:
            multiple = True

        if multiple:
            """
            Create multiple build outputs with a single quantity of 1
            """

            for ii in range(quantity):

                if serials:
                    serial = serials[ii]
                else:
                    serial = None

                StockModels.StockItem.objects.create(
                    quantity=1,
                    location=location,
                    part=self.part,
                    build=self,
                    batch=batch,
                    serial=serial,
                    is_building=True,
                )

        else:
            """
            Create a single build output of the given quantity
            """

            StockModels.StockItem.objects.create(
                quantity=quantity,
                location=location,
                part=self.part,
                build=self,
                batch=batch,
                is_building=True
            )

        if self.status == BuildStatus.PENDING:
            self.status = BuildStatus.PRODUCTION
            self.save()

    @transaction.atomic
    def deleteBuildOutput(self, output):
        """
        Remove a build output from the database:

        - Unallocate any build items against the output
        - Delete the output StockItem
        """

        if not output:
            raise ValidationError(_("No build output specified"))

        if not output.is_building:
            raise ValidationError(_("Build output is already completed"))

        if not output.build == self:
            raise ValidationError(_("Build output does not match Build Order"))

        # Unallocate all build items against the output
        self.unallocateStock(output)

        # Remove the build output from the database
        output.delete()

    @transaction.atomic
    def autoAllocate(self, output):
        """
        Run auto-allocation routine to allocate StockItems to this Build.

        Args:
            output: If specified, only auto-allocate against the given built output

        Returns a list of dict objects with keys like:

            {
                'stock_item': item,
                'quantity': quantity,
            }

        See: getAutoAllocations()
        """

        allocations = self.getAutoAllocations(output)

        for item in allocations:
            # Create a new allocation
            build_item = BuildItem(
                build=self,
                stock_item=item['stock_item'],
                quantity=item['quantity'],
                install_into=output,
            )

            build_item.save()

    @transaction.atomic
    def completeBuildOutput(self, output, user, **kwargs):
        """
        Complete a particular build output

        - Remove allocated StockItems
        - Mark the output as complete
        """

        # Select the location for the build output
        location = kwargs.get('location', self.destination)

        # List the allocated BuildItem objects for the given output
        allocated_items = output.items_to_install.all()

        for build_item in allocated_items:

            # TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete
            # TODO: Use celery / redis to offload the actual object deletion...
            # REF: https://www.botreetechnologies.com/blog/implementing-celery-using-django-for-background-task-processing
            # REF: https://code.tutsplus.com/tutorials/using-celery-with-django-for-background-task-processing--cms-28732

            # Complete the allocation of stock for that item
            build_item.complete_allocation(user)

        # Delete the BuildItem objects from the database
        allocated_items.all().delete()

        # Ensure that the output is updated correctly
        output.build = self
        output.is_building = False
        output.location = location

        output.save()

        output.addTransactionNote(
            _('Completed build output'),
            user,
            system=True
        )

        # Increase the completed quantity for this build
        self.completed += output.quantity
        self.save()

    def requiredQuantity(self, part, output):
        """
        Get the quantity of a part required to complete the particular build output.

        Args:
            part: The Part object
            output - The particular build output (StockItem)
        """

        # Extract the BOM line item from the database
        try:
            bom_item = PartModels.BomItem.objects.get(part=self.part.pk, sub_part=part.pk)
            quantity = bom_item.quantity
        except (PartModels.BomItem.DoesNotExist):
            quantity = 0

        if output:
            quantity *= output.quantity
        else:
            quantity *= self.remaining

        return quantity

    def allocatedItems(self, part, output):
        """
        Return all BuildItem objects which allocate stock of <part> to <output>

        Args:
            part - The part object
            output - Build output (StockItem).
        """

        allocations = BuildItem.objects.filter(
            build=self,
            stock_item__part=part,
            install_into=output,
        )

        return allocations

    def allocatedQuantity(self, part, output):
        """
        Return the total quantity of given part allocated to a given build output.
        """

        allocations = self.allocatedItems(part, output)

        allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0))

        return allocated['q']

    def unallocatedQuantity(self, part, output):
        """
        Return the total unallocated (remaining) quantity of a part against a particular output.
        """

        required = self.requiredQuantity(part, output)
        allocated = self.allocatedQuantity(part, output)

        return max(required - allocated, 0)

    def isPartFullyAllocated(self, part, output):
        """
        Returns True if the part has been fully allocated to the particular build output
        """

        return self.unallocatedQuantity(part, output) == 0

    def isFullyAllocated(self, output):
        """
        Returns True if the particular build output is fully allocated.
        """

        for bom_item in self.bom_items:
            part = bom_item.sub_part

            if not self.isPartFullyAllocated(part, output):
                return False

        # All parts must be fully allocated!
        return True

    def allocatedParts(self, output):
        """
        Return a list of parts which have been fully allocated against a particular output
        """

        allocated = []

        for bom_item in self.bom_items:
            part = bom_item.sub_part

            if self.isPartFullyAllocated(part, output):
                allocated.append(part)

        return allocated

    def unallocatedParts(self, output):
        """
        Return a list of parts which have *not* been fully allocated against a particular output
        """

        unallocated = []

        for bom_item in self.bom_items:
            part = bom_item.sub_part

            if not self.isPartFullyAllocated(part, output):
                unallocated.append(part)

        return unallocated

    @property
    def required_parts(self):
        """ Returns a dict of parts required to build this part (BOM) """
        parts = []

        for item in self.part.bom_items.all().prefetch_related('sub_part'):
            parts.append(item.sub_part)

        return parts

    def availableStockItems(self, part, output):
        """
        Returns stock items which are available for allocation to this build.

        Args:
            part - Part object
            output - The particular build output
        """

        # Grab initial query for items which are "in stock" and match the part
        items = StockModels.StockItem.objects.filter(
            StockModels.StockItem.IN_STOCK_FILTER
        )

        items = items.filter(part=part)

        # Exclude any items which have already been allocated
        allocated = BuildItem.objects.filter(
            build=self,
            stock_item__part=part,
            install_into=output,
        )

        items = items.exclude(
            id__in=[item.stock_item.id for item in allocated.all()]
        )

        # Limit query to stock items which are "downstream" of the source location
        if self.take_from is not None:
            items = items.filter(
                location__in=[loc for loc in self.take_from.getUniqueChildren()]
            )

        # Exclude expired stock items
        if not common.models.InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_BUILD'):
            items = items.exclude(StockModels.StockItem.EXPIRED_FILTER)

        return items

    @property
    def is_active(self):
        """ Is this build active? An active build is either:

        - PENDING
        - HOLDING
        """

        return self.status in BuildStatus.ACTIVE_CODES

    @property
    def is_complete(self):
        """ Returns True if the build status is COMPLETE """

        return self.status == BuildStatus.COMPLETE
예제 #6
0
class Category(MPTTModel):
    """
    Simple model for categorizing entries.
    """

    title = models.CharField(_('title'), max_length=255)

    slug = models.SlugField(_('slug'),
                            unique=True,
                            max_length=255,
                            help_text=_("Used to build the category's URL."))

    description = models.TextField(_('description'), blank=True)

    parent = TreeForeignKey('self',
                            related_name='children',
                            null=True,
                            blank=True,
                            on_delete=models.SET_NULL,
                            verbose_name=_('parent category'))

    objects = TreeManager()
    published = EntryRelatedPublishedManager()

    def entries_published(self):
        """
        Returns category's published entries.
        """
        return entries_published(self.entries)

    @property
    def tree_path(self):
        """
        Returns category's tree path
        by concatening the slug of his ancestors.
        """
        if self.parent_id:
            return '/'.join(
                [ancestor.slug
                 for ancestor in self.get_ancestors()] + [self.slug])
        return self.slug

    @models.permalink
    def get_absolute_url(self):
        """
        Builds and returns the category's URL
        based on his tree path.
        """
        return ('zinnia:category_detail', (self.tree_path, ))

    def __str__(self):
        return self.title

    class Meta:
        """
        Category's meta informations.
        """
        ordering = ['title']
        verbose_name = _('category')
        verbose_name_plural = _('categories')

    class MPTTMeta:
        """
        Category MPTT's meta informations.
        """
        order_insertion_by = ['title']
예제 #7
0
class Document(AbstractDocument):
    user = models.ForeignKey('profiles.User',
                             verbose_name=_('user'),
                             related_name='documents')
    account = models.ForeignKey('accounts.Account',
                                verbose_name=_('account'),
                                null=True,
                                related_name='documents')
    previous_version = models.PositiveIntegerField(blank=True, null=True)
    folder = TreeForeignKey(Folder,
                            verbose_name=_('folder'),
                            related_name='documents',
                            blank=True,
                            null=True,
                            on_delete=models.SET_NULL)
    permissions = GenericRelation('permissions.ObjectPermission')
    approvals = models.ManyToManyField('profiles.User', through='Approval')

    def get_committee_name(self):
        if self.committee:
            return self.committee.name
        else:
            return _('All Board Members')

    @property
    def revisions(self):
        if hasattr(self, '_revision_cache'):
            return self._revision_cache
        else:
            return list(
                AuditTrail.objects.filter(
                    latest_version=self.id).order_by('-created_at'))

    @staticmethod
    def prefetch_revisions(documents):
        all_revisions = AuditTrail.objects.filter(
            latest_version__in=[d.id
                                for d in documents]).order_by('-created_at')
        revision_by_document_id = {}
        for revision in all_revisions:
            revision_by_document_id.setdefault(revision.latest_version,
                                               []).append(revision)

        for document in documents:
            document._revision_cache = revision_by_document_id.get(
                document.id, [])

    def send_notification_email(self, members):

        ctx_dict = {
            'document': self,
            'site': Site.objects.get_current(),
            'protocol': settings.SSL_ON and 'https' or 'http',
            'previous_versions': self.revisions
        }

        for member in members:
            tmpl = TemplateModel.objects.get(
                name=TemplateModel.DOCUMENT_UPDATED)
            subject = tmpl.title or self.account.name  # fixme: which one?
            message = tmpl.generate(ctx_dict)

            mail = EmailMessage(subject, message, settings.DEFAULT_FROM_EMAIL,
                                [member.user.email])
            mail.content_subtype = "html"

            mail.send()

    def approved_user_ids(self):
        """ """
        return self.approval_set.values_list('user_id', flat=True).distinct()
예제 #8
0
class Genre(MPTTModel):
    name = models.CharField(max_length=50, unique=True)
    parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')

    class MPTTMeta:
        order_insertion_by = ['name']
예제 #9
0
class treeItem(MPTTModel):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    parent = TreeForeignKey('self', null = True, blank = True, related_name = 'children', on_delete=models.CASCADE)
예제 #10
0
class WebSite(
        six.with_metaclass(WebSiteMeta, MPTTModel, DiffMixin,
                           AbstractDuplicateAwareModel)):
    """ Web site object. Used to hold options for a whole website. """
    class Meta:
        app_label = 'core'
        verbose_name = _(u'Web site')
        verbose_name_plural = _(u'Web sites')
        translate = (
            'short_description',
            'description',
        )

    # HEADS UP: we exclude the URL from inplace_edit form, else the
    # Django URLField will always add a trailing slash to domain-only
    # websites while we remove them. This is probably a bug in our heads
    # (we shouldn't remove the trailing slash on domain-only URLs ?),
    # but we want it to work this way for now.
    #
    # For 'mail_warned', it's the traditional JSONField + inplace error.
    INPLACEEDIT_EXCLUDE = [
        'url',
        'mail_warned',
    ]

    # class MPTTMeta:
    #     order_insertion_by = ['url']

    name = models.CharField(max_length=128,
                            verbose_name=_(u'name'),
                            null=True,
                            blank=True)

    slug = models.CharField(max_length=128,
                            verbose_name=_(u'slug'),
                            null=True,
                            blank=True)
    url = models.URLField(unique=True, verbose_name=_(u'url'), blank=True)

    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children')

    # TODO: move this into Website to avoid too much parallel fetches
    # when using multiple feeds from the same origin website.
    fetch_limit_nr = models.IntegerField(
        default=config.FEED_FETCH_PARALLEL_LIMIT,
        verbose_name=_(u'fetch limit'),
        blank=True,
        help_text=_(u'The maximum number of articles that can be fetched '
                    u'from the website in parallel. If less than {0}, do '
                    u'not touch: the workers have already tuned it from '
                    u'real-life results.').format(
                        config.FEED_FETCH_PARALLEL_LIMIT))

    mail_warned = JSONField(default=list, blank=True)

    date_created = models.DateTimeField(
        auto_now_add=True,
        db_index=True,
        verbose_name=_(u'Date added'),
        help_text=_(u'When the web site was added to the 1flow database.'))

    date_updated = models.DateTimeField(
        auto_now=True,
        verbose_name=_(u'Date updated'),
        help_text=_(u'When the web site was updated.'))

    image = models.ImageField(
        verbose_name=_(u'Image'),
        null=True,
        blank=True,
        upload_to=get_website_image_upload_path,
        max_length=256,
        help_text=_(u'Use either image when 1flow instance hosts the '
                    u'image, or image_url when hosted elsewhere. If '
                    u'both are filled, image takes precedence.'))

    image_url = models.URLField(
        null=True,
        blank=True,
        max_length=384,
        verbose_name=_(u'Image URL'),
        help_text=_(u'Full URL of the image displayed in the feed '
                    u'selector. Can be hosted outside of 1flow.'))

    short_description = models.CharField(
        null=True,
        blank=True,
        max_length=256,
        verbose_name=_(u'Short description'),
        help_text=_(u'Public short description of the feed, for '
                    u'auto-completer listing. Markdown text.'))

    description = models.TextField(
        null=True,
        blank=True,
        verbose_name=_(u'Description'),
        help_text=_(u'Public description of the feed. Markdown text.'))

    processing_chain = models.ForeignKey(ProcessingChain,
                                         null=True,
                                         blank=True,
                                         related_name='websites')

    processing_parameters = YAMLField(
        null=True,
        blank=True,
        verbose_name=_(u'Processing parameters'),
        help_text=_(u'Processing parameters for this website. '
                    u'Can be left empty. As they are more specific, '
                    u'the website parameters take precedence over the '
                    u'processors parameters, but will be overriden by '
                    u'feed-level or item-level processing parameters, '
                    u'if any. In YAML format (see '
                    u'http://en.wikipedia.org/wiki/YAML for details).'))

    # ————————————————————————————————————————————————————————— Python & Django

    def __unicode__(self):
        """ I'm __unicode__, pep257. """

        return u'%s #%s (%s)%s' % (self.name or u'WebSite', self.id, self.url,
                                   (_(u'(dupe of #%s)') % self.duplicate_of.id)
                                   if self.duplicate_of else u'')

    # ——————————————————————————————————————————————————————————— Class methods

    @classmethod
    def get_from_url(cls, url):
        """ Will get you the ``Website`` object from an :param:`url`.

        After having striped down the path part (eg.
        ``http://test.com/my-article`` gives you the web site
        ``http://test.com``, without the trailing slash).

        It will return ``None`` if the url is really bad.

        .. note:: unlike :meth:`get_or_create_website`, this method will
            harmonize urls: ``Website.get_from_url('http://toto.com')``
            and  ``Website.get_from_url('http://toto.com/')`` will give
            you back the same result. This is intended, to avoid
            duplication.

        """

        try:
            proto, host_and_port, remaining = split_url(url)

        except:
            LOGGER.exception(u'Unable to split url “%s”', url)
            return None

        base_url = '%s://%s' % (proto, host_and_port)

        @cached_as(WebSite, timeout=3600, extra=base_url)
        def _get_website_from_url(base_url):

            try:
                website, _ = WebSite.objects.get_or_create(url=base_url)

            except:
                LOGGER.exception(
                    'Could not get or create website from url '
                    u'“%s” (via original “%s”)', base_url, url)
                return None

            return website

        try:
            return _get_website_from_url(base_url)

        except TypeError:
            return None

    def to_json(self, related_to=None):

        return OrderedDict(
            id=unicode(self.id),
            name=self.name,
            slug=self.slug,
            url=self.url,
            image_url=self.image_url,
            short_description=self.short_description,
        )
예제 #11
0
class Post(MPTTModel, NamedModel):
    uid = models.UUIDField(max_length=8,
                           primary_key=True,
                           default=gen_uuid,
                           editable=False)
    content = models.TextField(blank=True, default='')
    created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                   null=True,
                                   blank=True,
                                   on_delete=models.CASCADE,
                                   related_name='+')
    created_on = models.DateTimeField(auto_now_add=True, auto_now=False)
    modified_on = models.DateTimeField(auto_now_add=False, auto_now=True)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            db_index=True,
                            on_delete=models.CASCADE)
    _upvotes = models.IntegerField(blank=True, default=0)
    _downvotes = models.IntegerField(blank=True, default=0)
    wsi = models.FloatField(blank=True, default=0)  # Wilson score interval
    ip_address = models.GenericIPAddressField(blank=True, null=True)
    user_agent = models.CharField(max_length=150, blank=True, null=True)

    def __init__(self, *args, **kwargs):
        super(Post, self).__init__(*args, **kwargs)
        Post.upvotes = property(lambda self: self._upvotes,
                                Post._voteSetterWrapper('_upvotes'))
        Post.downvotes = property(lambda self: self._downvotes,
                                  Post._voteSetterWrapper('_downvotes'))

    class MPTTMetta:
        order_insertion_by = ['created_on']

    def __str__(self):
        return self.content[:70]

    @staticmethod
    def _voteSetterWrapper(attr):
        def voteSetter(self, value):
            setattr(self, attr, max(0, value))
            self.wsi = wsi_confidence(self._upvotes, self._downvotes)

        return voteSetter

    @property
    def thread(self):  # TODO: thread should be stored in Post
        post = self
        while post.parent:
            post = post.parent
        return Thread.objects.get(op=post)

    @property
    def score(self):
        return self.upvotes - self.downvotes

    def getReplies(self, excluded=()):
        """:param excluded: exclude all posts with these uids and their descendants"""
        replies = Post.objects.filter(parent=self.uid).exclude(
            uid__in=excluded)
        for reply in replies:
            replies |= reply.getReplies(excluded=excluded)
        return replies

    def getSortedReplies(self, limit=50, by_wsi=True, excluded=()):
        """
        :param limit: number of replies to return
        :param by_wsi: sort replies by wsi score or by creation date
        :param excluded: uids or excluded replies
        """
        excluded = list(excluded)
        order_field = '-wsi' if by_wsi else 'created_on'
        replies = list(
            self.getReplies(excluded=excluded).order_by(order_field)[:limit])
        self._getPostsWithChildren(replies)
        sorted_replies = []
        for p in replies:
            sorted_replies.append(p)
            sorted_replies += p.getChildrenList()
        return sorted_replies

    def _getPostsWithChildren(self, replies):
        for p in list(replies):
            if not hasattr(p, 'included_children'):
                p.included_children = []
            if p.parent != self:
                if p.parent not in replies:
                    # add missing parents
                    current_p = p
                    while True:
                        current_p.parent._addToIncludedChildren(current_p)
                        current_p = current_p.parent
                        if current_p in (replies + [self]):
                            break
                    if current_p in replies:
                        replies[replies.index(current_p)] = current_p
                else:
                    p.parent = replies[replies.index(p.parent)]
                    p.parent._addToIncludedChildren(p)
                replies.remove(p)

    def _addToIncludedChildren(self, post):
        if not hasattr(self, 'included_children'):
            self.included_children = [post]
        else:
            self.included_children.append(post)

    def getChildrenList(self):
        children = []
        for p in self.included_children:
            children.append(p)
            if p.included_children:
                children += p.getChildrenList()
        return children

    def setMeta(self, request):
        """update post ip_address & user_agent attributes"""
        ip = get_ip(request)
        if ip is not None:
            self.ip_address = ip
        ua = request.META.get('HTTP_USER_AGENT', '')
        if ua:
            self.user_agent = ua
예제 #12
0
class Event(MPTTModel, BaseModel, SchemalessFieldMixin):
    jsonld_type = "Event/LinkedEvent"
    objects = BaseTreeQuerySet.as_manager()
    """
    eventStatus enumeration is based on http://schema.org/EventStatusType
    """
    class Status:
        SCHEDULED = 1
        CANCELLED = 2
        POSTPONED = 3
        RESCHEDULED = 4

    # Properties from schema.org/Event
    STATUSES = (
        (Status.SCHEDULED, "EventScheduled"),
        (Status.CANCELLED, "EventCancelled"),
        (Status.POSTPONED, "EventPostponed"),
        (Status.RESCHEDULED, "EventRescheduled"),
    )

    class SuperEventType:
        RECURRING = 'recurring'
        UMBRELLA = 'umbrella'

    SUPER_EVENT_TYPES = (
        (SuperEventType.RECURRING, _('Recurring')),
        (SuperEventType.UMBRELLA, _('Umbrella event')),
    )

    # Properties from schema.org/Thing
    info_url = models.URLField(verbose_name=_('Event home page'),
                               blank=True,
                               null=True,
                               max_length=1000)
    description = models.TextField(verbose_name=_('Description'),
                                   blank=True,
                                   null=True)
    short_description = models.TextField(verbose_name=_('Short description'),
                                         blank=True,
                                         null=True)

    # Properties from schema.org/CreativeWork
    date_published = models.DateTimeField(verbose_name=_('Date published'),
                                          null=True,
                                          blank=True)
    # headline and secondary_headline are for cases where
    # the original event data contains a title and a subtitle - in that
    # case the name field is combined from these.
    #
    # secondary_headline is mapped to schema.org alternative_headline
    # and is used for subtitles, that is for
    # secondary, complementary headlines, not "alternative" headlines
    headline = models.CharField(verbose_name=_('Headline'),
                                max_length=255,
                                null=True,
                                db_index=True)
    secondary_headline = models.CharField(verbose_name=_('Secondary headline'),
                                          max_length=255,
                                          null=True,
                                          db_index=True)
    provider = models.CharField(verbose_name=_('Provider'),
                                max_length=512,
                                null=True)
    provider_contact_info = models.CharField(
        verbose_name=_("Provider's contact info"),
        max_length=255,
        null=True,
        blank=True)
    publisher = models.ForeignKey('django_orghierarchy.Organization',
                                  verbose_name=_('Publisher'),
                                  db_index=True,
                                  on_delete=models.PROTECT,
                                  related_name='published_events')

    # Status of the event itself
    event_status = models.SmallIntegerField(verbose_name=_('Event status'),
                                            choices=STATUSES,
                                            default=Status.SCHEDULED)

    # Whether or not this data about the event is ready to be viewed by the general public.
    # DRAFT means the data is considered incomplete or is otherwise undergoing refinement --
    # or just waiting to be published for other reasons.
    publication_status = models.SmallIntegerField(
        verbose_name=_('Event data publication status'),
        choices=PUBLICATION_STATUSES,
        default=PublicationStatus.PUBLIC)

    location = models.ForeignKey(Place,
                                 related_name='events',
                                 null=True,
                                 blank=True,
                                 on_delete=models.PROTECT)
    location_extra_info = models.CharField(
        verbose_name=_('Location extra info'),
        max_length=400,
        null=True,
        blank=True)

    start_time = models.DateTimeField(verbose_name=_('Start time'),
                                      null=True,
                                      db_index=True,
                                      blank=True)
    end_time = models.DateTimeField(verbose_name=_('End time'),
                                    null=True,
                                    db_index=True,
                                    blank=True)
    has_start_time = models.BooleanField(default=True)
    has_end_time = models.BooleanField(default=True)

    audience_min_age = models.SmallIntegerField(
        verbose_name=_('Minimum recommended age'),
        blank=True,
        null=True,
        db_index=True)
    audience_max_age = models.SmallIntegerField(
        verbose_name=_('Maximum recommended age'),
        blank=True,
        null=True,
        db_index=True)

    super_event = TreeForeignKey('self',
                                 null=True,
                                 blank=True,
                                 on_delete=models.SET_NULL,
                                 related_name='sub_events')

    super_event_type = models.CharField(max_length=255,
                                        blank=True,
                                        null=True,
                                        db_index=True,
                                        default=None,
                                        choices=SUPER_EVENT_TYPES)

    in_language = models.ManyToManyField(Language,
                                         verbose_name=_('In language'),
                                         related_name='events',
                                         blank=True)

    images = models.ManyToManyField(Image, related_name='events', blank=True)

    deleted = models.BooleanField(default=False, db_index=True)

    # Custom fields not from schema.org
    keywords = models.ManyToManyField(Keyword, related_name='events')
    audience = models.ManyToManyField(Keyword,
                                      related_name='audience_events',
                                      blank=True)

    class Meta:
        verbose_name = _('event')
        verbose_name_plural = _('events')

    class MPTTMeta:
        parent_attr = 'super_event'

    def save(self, *args, **kwargs):
        # needed to cache location event numbers
        old_location = None
        if self.id:
            try:
                old_location = Event.objects.get(id=self.id).location
            except Event.DoesNotExist:
                pass

        # drafts may not have times set, so check that first
        start = getattr(self, 'start_time', None)
        end = getattr(self, 'end_time', None)
        if start and end:
            if start > end:
                raise ValidationError({
                    'end_time':
                    _('The event end time cannot be earlier than the start time.'
                      )
                })

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

        # needed to cache location event numbers
        if not old_location and self.location:
            Place.objects.filter(id=self.location.id).update(
                n_events_changed=True)
        if old_location and not self.location:
            # drafts (or imported events) may not always have location set
            Place.objects.filter(id=old_location.id).update(
                n_events_changed=True)
        if old_location and self.location and old_location != self.location:
            Place.objects.filter(id__in=(old_location.id,
                                         self.location.id)).update(
                                             n_events_changed=True)

    def __str__(self):
        name = ''
        languages = [lang[0] for lang in settings.LANGUAGES]
        for lang in languages:
            lang = lang.replace(
                '-', '_')  # to handle complex codes like e.g. zh-hans
            s = getattr(self, 'name_%s' % lang, None)
            if s:
                name = s
                break
        val = [name, '(%s)' % self.id]
        dcount = self.get_descendant_count()
        if dcount > 0:
            val.append(u" (%d children)" % dcount)
        else:
            val.append(str(self.start_time))
        return u" ".join(val)

    def is_admin(self, user):
        if user.is_superuser:
            return True
        else:
            return user.is_admin(self.publisher)

    def can_be_edited_by(self, user):
        """Check if current event can be edited by the given user"""
        if user.is_superuser:
            return True
        return user.can_edit_event(self.publisher, self.publication_status)

    def soft_delete(self, using=None):
        self.deleted = True
        self.save(update_fields=("deleted", ), using=using, force_update=True)

    def undelete(self, using=None):
        self.deleted = False
        self.save(update_fields=("deleted", ), using=using, force_update=True)
예제 #13
0
class Place(MPTTModel, BaseModel, SchemalessFieldMixin, ImageMixin):
    objects = BaseTreeQuerySet.as_manager()
    geo_objects = objects

    publisher = models.ForeignKey('django_orghierarchy.Organization',
                                  verbose_name=_('Publisher'),
                                  db_index=True)
    info_url = models.URLField(verbose_name=_('Place home page'),
                               null=True,
                               blank=True,
                               max_length=1000)
    description = models.TextField(verbose_name=_('Description'),
                                   null=True,
                                   blank=True)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children')

    position = models.PointField(srid=settings.PROJECTION_SRID,
                                 null=True,
                                 blank=True)

    email = models.EmailField(verbose_name=_('E-mail'), null=True, blank=True)
    telephone = models.CharField(verbose_name=_('Telephone'),
                                 max_length=128,
                                 null=True,
                                 blank=True)
    contact_type = models.CharField(verbose_name=_('Contact type'),
                                    max_length=255,
                                    null=True,
                                    blank=True)
    street_address = models.CharField(verbose_name=_('Street address'),
                                      max_length=255,
                                      null=True,
                                      blank=True)
    address_locality = models.CharField(verbose_name=_('Address locality'),
                                        max_length=255,
                                        null=True,
                                        blank=True)
    address_region = models.CharField(verbose_name=_('Address region'),
                                      max_length=255,
                                      null=True,
                                      blank=True)
    postal_code = models.CharField(verbose_name=_('Postal code'),
                                   max_length=128,
                                   null=True,
                                   blank=True)
    post_office_box_num = models.CharField(verbose_name=_('PO BOX'),
                                           max_length=128,
                                           null=True,
                                           blank=True)
    address_country = models.CharField(verbose_name=_('Country'),
                                       max_length=2,
                                       null=True,
                                       blank=True)

    deleted = models.BooleanField(verbose_name=_('Deleted'), default=False)
    replaced_by = models.ForeignKey('Place', related_name='aliases', null=True)
    divisions = models.ManyToManyField(AdministrativeDivision,
                                       verbose_name=_('Divisions'),
                                       related_name='places',
                                       blank=True)
    n_events = models.IntegerField(
        verbose_name=_('event count'),
        help_text=_('number of events in this location'),
        default=0,
        editable=False,
        db_index=True)
    n_events_changed = models.BooleanField(default=False, db_index=True)

    class Meta:
        verbose_name = _('place')
        verbose_name_plural = _('places')
        unique_together = (('data_source', 'origin_id'), )

    def __unicode__(self):
        values = filter(
            lambda x: x,
            [self.street_address, self.postal_code, self.address_locality])
        return u', '.join(values)

    @transaction.atomic
    def save(self, *args, **kwargs):
        if self.replaced_by and self.replaced_by.replaced_by == self:
            raise Exception(
                "Trying to replace the location replacing this location by this location."
                "Please refrain from creating circular replacements and"
                "remove either one of the replacements."
                "We don't want homeless events.")

        # needed to remap events to replaced location
        old_replaced_by = None
        if self.id:
            try:
                old_replaced_by = Place.objects.get(id=self.id).replaced_by
            except Place.DoesNotExist:
                pass

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

        # needed to remap events to replaced location
        if not old_replaced_by == self.replaced_by:
            Event.objects.filter(location=self).update(
                location=self.replaced_by)
            # Update doesn't call save so we update event numbers manually.
            # Not all of the below are necessarily present.
            ids_to_update = [
                event.id for event in (self, self.replaced_by, old_replaced_by)
                if event
            ]
            Place.objects.filter(id__in=ids_to_update).update(
                n_events_changed=True)

        if self.position:
            self.divisions.set(
                AdministrativeDivision.objects.filter(
                    type__type__in=('district', 'sub_district', 'neighborhood',
                                    'muni'),
                    geometry__boundary__contains=self.position))
        else:
            self.divisions.clear()
예제 #14
0
class MPTTArticle(MPTTModel, Article):

    parent = TreeForeignKey('self', null=True, blank=True, related_name='children')

    class MPTTMeta:
        order_insertion_by=['order']

    class Meta:
        ordering=['tree_id','lft']

    def get_absolute_url(self):
        course_pk = None
        if self.course and self.course.pk:
            course_pk = self.course.pk
        return reverse("textcourse:article_detail", kwargs={"pk1": course_pk, "pk": self.pk})    

    def get_absolute_url_list(self):
        course_pk = None
        if hasattr(self, 'course') and self.course and self.course.pk:
            course_pk = self.course.pk
            return reverse("textcourse:course_detail", kwargs={"pk": course_pk})
        else:
            return reverse("textcourse:course_list", kwargs={})

    def get_absolute_url_update(self):
        course_pk = None
        if self.course and self.course.pk:
            course_pk = self.course.pk
        return reverse("textcourse:article_update", kwargs={"pk1": course_pk, "pk": self.pk})  

    def get_absolute_url_delete(self):
        course_pk = None
        if self.course and self.course.pk:
            course_pk = self.course.pk
        return reverse("textcourse:article_delete", kwargs={"pk1": course_pk, "pk": self.pk}) 

    def  __unicode__(self):
        return "{} - {}".format(self.get_index(), self.title)

    def  __str__(self):
        return "{} - {}".format(self.get_index(), self.title)

    def get_next_by_order(self, *args, **kwargs):
        field = self.__class__._meta.get_field('order')        
        try:
            return self._get_next_or_previous_by_FIELD(field, is_next=True, parent=None, course=self.course)
        except MPTTArticle.DoesNotExist:
            return None

    def get_previous_by_order(self, *args, **kwargs):
        field = self.__class__._meta.get_field('order')
        try:
            return self._get_next_or_previous_by_FIELD(field, is_next=False, parent=None, course=self.course)
        except MPTTArticle.DoesNotExist:
            return None

    @property
    def previous(self): 
        try:
            if self.is_root_node():
                node = self.get_previous_by_order()
                if node:
                    if node.get_children():
                        return node.get_children().last()
                    else:
                        return node
            else:
                if self.get_previous_sibling():
                    if self.get_previous_sibling().get_children():
                        return self.get_previous_sibling().get_children().last()
                    else:
                        return self.get_previous_sibling()
                elif self.parent:
                    return self.parent
                else:
                    pass
        except:
            pass

        return None

    @property
    def next(self):        
        try:
            if self.is_root_node():

                if self.get_children():
                    return self.get_children().first()
                else:
                    return self.get_next_by_order()      
            else:
                if self.get_children():
                    return self.get_children().first()
                elif not self.get_next_sibling():
                    if self.parent.is_root_node():
                        return self.parent.get_next_by_order()
                    else:
                        return self.parent.get_next_sibling()
                else:
                    return self.get_next_sibling()
        except:
            pass

        return None    

    objects = ArticleManager()
예제 #15
0
class Post(models.Model):
    author = models.ForeignKey(User,
                               default=1,
                               on_delete=models.CASCADE,
                               related_name='authorpost')
    category = TreeForeignKey(Category,
                              related_name='postcategory',
                              on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    body = models.TextField()
    read_time = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        editable=False,
    )
    draft = models.BooleanField(default=False)
    slug = models.SlugField(max_length=200,
                            db_index=True,
                            unique=True,
                            editable=False,
                            allow_unicode=True)
    publish_date = models.DateTimeField(auto_now=False,
                                        auto_now_add=False,
                                        null=True,
                                        blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    objects = PostManager()
    tags = TaggableManager()

    class Meta:
        ordering = ['-publish_date', '-updated', '-timestamp']
        index_together = (('id', 'slug'), )

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        self.read_time = get_read_time(self.body)
        super().save(*args, **kwargs)

    def get_markdown(self):
        return mark_safe(markdown(self.body))

    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={
            "slug": self.slug,
        })

    def get_slug_list(self):
        k = self.category
        breadcrumb = []
        while k is not None:
            breadcrumb.append(k)
            k = k.parent
        return breadcrumb
        # for i in range(len(breadcrumb) - 1):
        #     breadcrumb[i] = '/'.join(breadcrumb[-1:i-1:-1])
        # return breadcrumb[-1:0:-1]

    def __str__(self):
        return "{} publsihed on {}".format(self.title, self.timestamp)
예제 #16
0
파일: models.py 프로젝트: Hitman23/rapidpro
class AdminBoundary(MPTTModel, models.Model):
    """
    Represents a single administrative boundary (like a country, state or district)
    """

    LEVEL_COUNTRY = 0
    LEVEL_STATE = 1
    LEVEL_DISTRICT = 2
    LEVEL_WARD = 3

    # used to separate segments in a hierarchy of boundaries. Has the advantage of being a character in GSM7 and
    # being very unlikely to show up in an admin boundary name.
    PATH_SEPARATOR = ">"
    PADDED_PATH_SEPARATOR = " > "

    osm_id = models.CharField(
        max_length=15, unique=True, help_text="This is the OSM id for this administrative boundary"
    )

    name = models.CharField(max_length=128, help_text="The name of our administrative boundary")

    level = models.IntegerField(
        help_text="The level of the boundary, 0 for country, 1 for state, 2 for district, 3 for ward"
    )

    parent = TreeForeignKey(
        "self",
        null=True,
        on_delete=models.PROTECT,
        blank=True,
        related_name="children",
        db_index=True,
        help_text="The parent to this political boundary if any",
    )

    path = models.CharField(max_length=768, help_text="The full path name for this location")

    geometry = models.MultiPolygonField(null=True, help_text="The full geometry of this administrative boundary")

    simplified_geometry = models.MultiPolygonField(
        null=True, help_text="The simplified geometry of this administrative boundary"
    )

    objects = NoGeometryManager()
    geometries = GeometryManager()

    @staticmethod
    def get_geojson_dump(features):
        # build a feature collection
        feature_collection = geojson.FeatureCollection(features)
        return geojson.dumps(feature_collection)

    def as_json(self):
        result = dict(osm_id=self.osm_id, name=self.name, level=self.level, aliases="")

        if self.parent:
            result["parent_osm_id"] = self.parent.osm_id

        aliases = "\n".join([alias.name for alias in self.aliases.all()])
        result["aliases"] = aliases
        return result

    def get_geojson_feature(self):
        return geojson.Feature(
            properties=dict(name=self.name, osm_id=self.osm_id, id=self.pk, level=self.level),
            zoomable=True if self.children.all() else False,
            geometry=None if not self.simplified_geometry else geojson.loads(self.simplified_geometry.geojson),
        )

    def get_geojson(self):
        return AdminBoundary.get_geojson_dump([self.get_geojson_feature()])

    def get_children_geojson(self):
        children = []
        for child in self.children.all():
            children.append(child.get_geojson_feature())
        return AdminBoundary.get_geojson_dump(children)

    def update(self, **kwargs):
        AdminBoundary.objects.filter(id=self.id).update(**kwargs)

        # update our object values so that self is up to date
        for key, value in kwargs.items():
            setattr(self, key, value)

    def update_path(self):
        if self.level == 0:
            self.path = self.name
            self.save(update_fields=("path",))

        def _update_child_paths(boundary):
            boundaries = AdminBoundary.objects.filter(parent=boundary).only("name", "parent__path")
            boundaries.update(
                path=Concat(Value(boundary.path), Value(" %s " % AdminBoundary.PATH_SEPARATOR), F("name"))
            )
            for boundary in boundaries:
                _update_child_paths(boundary)

        _update_child_paths(self)

    def release(self):
        AdminBoundary.objects.filter(parent=self).update(parent=None)
        self.delete()

    @classmethod
    def create(cls, osm_id, name, level, parent=None, **kwargs):
        """
        Create method that takes care of creating path based on name and parent
        """
        path = name
        if parent is not None:
            path = parent.path + AdminBoundary.PADDED_PATH_SEPARATOR + name

        return AdminBoundary.objects.create(osm_id=osm_id, name=name, level=level, parent=parent, path=path, **kwargs)

    @classmethod
    def strip_last_path(cls, path):
        """
        Strips the last part of the passed in path. Throws if there is no separator
        """
        parts = path.split(AdminBoundary.PADDED_PATH_SEPARATOR)
        if len(parts) <= 1:  # pragma: no cover
            raise Exception("strip_last_path called without a path to strip")

        return AdminBoundary.PADDED_PATH_SEPARATOR.join(parts[:-1])

    @classmethod
    def get_by_path(cls, org, path):
        cache = getattr(org, "_abs", {})

        if not cache:
            setattr(org, "_abs", cache)

        boundary = cache.get(path)
        if not boundary:
            boundary = AdminBoundary.objects.filter(path=path).first()
            cache[path] = boundary

        return boundary

    def __str__(self):
        return "%s" % self.name
예제 #17
0
class ProcessStep(MPTTModel, Process):
    Type_CHOICES = (
        (0, "Receive new object"),
        (5, "The object is ready to remodel"),
        (9, "New object stable"),
        (10, "Object don't exist in AIS"),
        (11, "Object don't have any projectcode in AIS"),
        (12, "Object don't have any local policy"),
        (13, "Object already have an AIP!"),
        (14, "Object is not active!"),
        (19, "Object got a policy"),
        (20, "Object not updated from AIS"),
        (21, "Object not accepted in AIS"),
        (24, "Object accepted in AIS"),
        (25, "SIP validate"),
        (30, "Create AIP package"),
        (40, "Create package checksum"),
        (50, "AIP validate"),
        (60, "Try to remove IngestObject"),
        (1000, "Write AIP to longterm storage"),
        (1500, "Remote AIP"),
        (2009, "Remove temp AIP object OK"),
        (3000, "Archived"),
        (5000, "ControlArea"),
        (5100, "WorkArea"),
        (9999, "Deleted"),
    )

    type = models.IntegerField(null=True, choices=Type_CHOICES)
    user = models.CharField(max_length=45)
    parent_step = TreeForeignKey('self',
                                 related_name='child_steps',
                                 on_delete=models.CASCADE,
                                 null=True)
    parent_step_pos = models.IntegerField(_('Parent step position'), default=0)
    information_package = models.ForeignKey('ip.InformationPackage',
                                            on_delete=models.CASCADE,
                                            related_name='steps',
                                            blank=True,
                                            null=True)
    parallel = models.BooleanField(default=False)
    on_error = models.ManyToManyField('ProcessTask',
                                      related_name='steps_on_errors')
    context = jsonfield.JSONField(default={}, null=True)

    def get_pos(self):
        return self.parent_step_pos

    def get_descendants_tasks(self):
        steps = self.get_descendants(include_self=True)
        return ProcessTask.objects.filter(processstep__in=steps)

    def add_tasks(self, *tasks):
        self.clear_cache()
        self.tasks.add(*tasks)

    def remove_tasks(self, *tasks):
        self.clear_cache()
        self.tasks.remove(*tasks)

    def clear_tasks(self):
        self.clear_cache()
        self.tasks.clear()

    def add_child_steps(self, *steps):
        self.clear_cache()
        self.child_steps.add(*steps)

    def remove_child_steps(self, *steps):
        self.clear_cache()
        self.child_steps.remove(*steps)

    def clear_child_steps(self):
        self.clear_cache()
        self.child_steps.clear()

    def task_set(self):
        """
        Gets the unique tasks connected to the process, ignoring retries and
        undos.

        Returns:
            Unique tasks connected to the process, ignoring retries and undos
        """
        return self.tasks.filter(
            undo_type=False, retried__isnull=True).order_by("processstep_pos")

    def clear_cache(self):
        """
        Clears the cache for this step and all its ancestors
        """

        cache.delete(self.cache_status_key)
        cache.delete(self.cache_progress_key)

        if self.parent_step:
            self.parent_step.clear_cache()

    def run_children(self, tasks, steps, direct=True):

        if not tasks.exists() and not steps.exists():
            if direct:
                return EagerResult(self.pk, [], celery_states.SUCCESS)

            return group()

        func = group if self.parallel else chain
        result_list = sorted(itertools.chain(steps, tasks),
                             key=lambda x: (x.get_pos(), x.time_created))

        on_error_tasks = self.on_error(manager='by_step_pos').all()
        if on_error_tasks.exists():
            on_error_group = group(
                create_sub_task(t, self, immutable=False)
                for t in on_error_tasks)
        else:
            on_error_group = None

        if direct:
            logger.debug('Creating celery workflow')
        else:
            logger.debug('Creating partial celery workflow')

        workflow = func(
            y
            for y in (x.resume(direct=False) if isinstance(x, ProcessStep) else
                      create_sub_task(x, self, link_error=on_error_group)
                      for x in result_list)
            if not hasattr(y, 'tasks') or len(y.tasks))

        if direct:
            logger.info('Celery workflow created')
        else:
            logger.info('Partial celery workflow created')

        if direct:
            if self.eager:
                logger.info('Running workflow eagerly')
                return workflow.apply(link_error=on_error_group)
            else:
                logger.info('Running workflow non-eagerly')
                return workflow.apply_async(link_error=on_error_group)
        else:
            return workflow

    def run(self, direct=True):
        """
        Runs the process step by first running the child steps and then the
        tasks.

        Args:
            direct: False if the step is called from a parent step,
                    true otherwise

        Returns:
            The executed workflow consisting of potential child steps followed by
            tasks if called directly. The workflow "non-executed" if direct is
            false
        """

        child_steps = self.child_steps.all()
        tasks = self.tasks(manager='by_step_pos').all()

        return self.run_children(tasks, child_steps, direct)

    def undo(self, only_failed=False, direct=True):
        """
        Undos the process step by first undoing all tasks and then the
        child steps.

        Args:
            only_failed: If true, only undo the failed tasks,
                undo all tasks otherwise

        Returns:
            AsyncResult/EagerResult if there is atleast one task or child
            steps, otherwise None
        """

        child_steps = self.child_steps.all()
        tasks = self.tasks(manager='by_step_pos').all()

        if only_failed:
            tasks = tasks.filter(status=celery_states.FAILURE)

        tasks = tasks.filter(undo_type=False, undone__isnull=True)

        if not tasks.exists() and not child_steps.exists():
            if direct:
                return EagerResult(self.pk, [], celery_states.SUCCESS)

            return group()

        func = group if self.parallel else chain

        result_list = sorted(itertools.chain(child_steps, tasks),
                             key=lambda x: (x.get_pos(), x.time_created),
                             reverse=True)
        workflow = func(
            x.undo(only_failed=only_failed, direct=False) if isinstance(
                x, ProcessStep) else create_sub_task(x.create_undo_obj(), self)
            for x in result_list)

        if direct:
            if self.eager:
                return workflow.apply()
            else:
                return workflow.apply_async()
        else:
            return workflow

    def retry(self, direct=True):
        """
        Retries the process step by first retrying all child steps and then all
        failed tasks.

        Args:
            direct: False if the step is called from a parent step,
                    true otherwise

        Returns:
            none
        """

        child_steps = self.child_steps.all()

        tasks = self.tasks(manager='by_step_pos').filter(
            undone__isnull=False,
            retried__isnull=True).order_by('processstep_pos')

        if not tasks.exists() and not child_steps.exists():
            if direct:
                return EagerResult(self.pk, [], celery_states.SUCCESS)

            return group()

        func = group if self.parallel else chain

        result_list = sorted(itertools.chain(child_steps, tasks),
                             key=lambda x: (x.get_pos(), x.time_created))
        workflow = func(
            x.retry(direct=False) if isinstance(x, ProcessStep) else
            create_sub_task(x.create_retry_obj(), self) for x in result_list)

        if direct:
            if self.eager:
                return workflow.apply()
            else:
                return workflow.apply_async()
        else:
            return workflow

    def resume(self, direct=True):
        """
        Resumes the process step by running all pending child steps and tasks

        Args:
            direct: False if the step is called from a parent step,
                    true otherwise

        Returns:
            The executed workflow if direct is true, the workflow non-executed
            otherwise
        """

        logger.debug('Resuming step {} ({})'.format(self.name, self.pk))
        child_steps = self.get_children()
        tasks = self.tasks(manager='by_step_pos').filter(
            undone__isnull=True, undo_type=False, status=celery_states.PENDING)

        return self.run_children(tasks, child_steps, direct)

    @property
    def cache_lock_key(self):
        return '%s_lock' % str(self.pk)

    @property
    def cache_status_key(self):
        return '%s_status' % str(self.pk)

    @property
    def cache_progress_key(self):
        return '%s_progress' % str(self.pk)

    @property
    def time_started(self):
        if self.tasks.exists():
            return self.tasks.first().time_started

    @property
    def time_done(self):
        if self.tasks.exists():
            return self.tasks.first().time_done

    @property
    def progress(self):
        """
        Gets the progress of the step based on its child steps and tasks

        Args:

        Returns:
            The progress calculated by progress/total where progress simply is
            the progress (0-100) of all the underlying tasks and the total is
            |child_steps| + |tasks|
        """

        with cache.lock(self.cache_lock_key, timeout=60):
            cached = cache.get(self.cache_progress_key)

            if cached is not None:
                return cached

            if not self.child_steps.exists() and not self.tasks.exists():
                progress = 0
                cache.set(self.cache_progress_key, progress)
                return progress

            child_steps = self.child_steps.all()
            progress = 0
            task_data = self.tasks.filter(
                undo_type=False,
                retried__isnull=True).aggregate(progress=Sum(
                    Case(When(undone__isnull=False, then=0),
                         default='progress')),
                                                task_count=Count('id'))

            total = len(child_steps) + task_data['task_count']

            if total == 0:
                cache.set(self.cache_progress_key, 100)
                return 100

            progress += sum([c.progress for c in child_steps])

            try:
                progress += task_data['progress']
            except BaseException:
                pass

            try:
                res = progress / total
                cache.set(self.cache_progress_key, res)
                return res
            except BaseException:
                cache.set(self.cache_progress_key, 0)
                return 0

    @property
    def status(self):
        """
        Gets the status of the step based on its child steps and tasks

        Args:

        Returns:
            Can be one of the following:
            SUCCESS, STARTED, FAILURE, PENDING

            Which is decided by five scenarios:

            * If there are no child steps nor tasks, then SUCCESS.
            * If there are child steps or tasks and they are all pending,
              then PENDING.
            * If a child step or task has started, then STARTED.
            * If a child step or task has failed, then FAILURE.
            * If all child steps and tasks have succeeded, then SUCCESS.
        """

        with cache.lock(self.cache_lock_key, timeout=60):
            cached = cache.get(self.cache_status_key)

            if cached is not None:
                return cached

            child_steps = self.child_steps.all()
            tasks = self.tasks.filter(undo_type=False,
                                      undone__isnull=True,
                                      retried__isnull=True)
            status = celery_states.SUCCESS

            if not child_steps.exists() and not tasks.exists():
                status = celery_states.PENDING
                cache.set(self.cache_status_key, status)
                return status

            if tasks.filter(status=celery_states.FAILURE).exists():
                cache.set(self.cache_status_key, celery_states.FAILURE)
                return celery_states.FAILURE

            if tasks.filter(status=celery_states.PENDING).exists():
                status = celery_states.PENDING

            if tasks.filter(status=celery_states.STARTED).exists():
                status = celery_states.STARTED

            for cs in child_steps.only('parent_step').iterator():
                if cs.status == celery_states.STARTED:
                    status = cs.status
                if (cs.status == celery_states.PENDING
                        and status != celery_states.STARTED):
                    status = cs.status
                if cs.status == celery_states.FAILURE:
                    cache.set(self.cache_status_key, cs.status)
                    return cs.status

            cache.set(self.cache_status_key, status)
            return status

    @property
    def undone(self):
        """
        Gets the undone state of the step based on its tasks and child steps

        Args:

        Returns:
            True if one or more child steps and/or tasks have undone set to
            true, false otherwise
        """

        for c in self.child_steps.iterator():
            if c.undone:
                return True

        if self.tasks.filter(undone__isnull=False,
                             retried__isnull=True).exists():
            return True

        return False

    class Meta:
        db_table = u'ProcessStep'
        ordering = ('parent_step_pos', 'time_created')
        get_latest_by = "time_created"

    class MPTTMeta:
        parent_attr = 'parent_step'
예제 #18
0
class Account(MPTTModel):
    """ Represents an account

    An account may have a parent, and may have zero or more children. Only root
    accounts can have a type, all child accounts are assumed to have the same
    type as their parent.

    An account's balance is calculated as the sum of all of the transaction Leg's
    referencing the account.

    Attributes:

        uuid (SmallUUID): UUID for account. Use to prevent leaking of IDs (if desired).
        name (str): Name of the account. Required.
        parent (Account|None): Parent account, nonen if root account
        code (str): Account code. Must combine with account codes of parent
            accounts to get fully qualified account code.
        type (str): Type of account as defined by :attr:`Account.TYPES`. Can only be set on
            root accounts. Child accounts are assumed to have the same time as their parent.
        TYPES (Choices): Available account types. Uses ``Choices`` from ``django-model-utils``. Types can be
            accessed in the form ``Account.TYPES.asset``, ``Account.TYPES.expense``, etc.
        is_bank_account (bool): Is this a bank account. This implies we can import bank statements into
            it and that it only supports a single currency.


    """
    TYPES = Choices(
        ('AS', 'asset', 'Asset'),  # Eg. Cash in bank
        ('LI', 'liability',
         'Liability'),  # Eg. Loans, bills paid after the fact (in arrears)
        ('IN', 'income', 'Income'),  # Eg. Sales, housemate contributions
        ('EX', 'expense', 'Expense'),  # Eg. Office supplies, paying bills
        ('EQ', 'equity', 'Equity'),  # Eg. Money from shares
        ('TR', 'trading', 'Currency Trading'
         )  # Used to represent currency conversions
    )
    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    name = models.CharField(max_length=50)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            db_index=True)
    code = models.CharField(max_length=3)
    full_code = models.CharField(max_length=100, db_index=True, unique=True)
    # TODO: Implement this child_code_width field, as it is probably a good idea
    # child_code_width = models.PositiveSmallIntegerField(default=1)
    type = models.CharField(max_length=2, choices=TYPES, blank=True)
    is_bank_account = models.BooleanField(
        default=False,
        blank=True,
        help_text='Is this a bank account. This implies we can import bank '
        'statements into it and that it only supports a single currency')
    currencies = ArrayField(models.CharField(max_length=3), db_index=True)

    objects = AccountManager.from_queryset(AccountQuerySet)()

    class MPTTMeta:
        order_insertion_by = ['code']

    class Meta:
        unique_together = (('parent', 'code'), )

    def __init__(self, *args, **kwargs):
        super(Account, self).__init__(*args, **kwargs)
        self._initial_code = self.code

    def save(self, *args, **kwargs):
        is_creating = not bool(self.pk)
        super(Account, self).save(*args, **kwargs)
        do_refresh = False

        # If we've just created a non-root node then we're going to need to load
        # the type back from the DB (as it is set by trigger)
        if is_creating and not self.is_root_node():
            do_refresh = True

        # If we've just create this account or if the code has changed then we're
        # going to need to reload from the DB (full_code is set by trigger)
        if is_creating or self._initial_code != self.code:
            do_refresh = True

        if do_refresh:
            self.refresh_from_db()

    @classmethod
    def validate_accounting_equation(cls):
        """Check that all accounts sum to 0"""
        balances = [
            account.balance(raw=True)
            for account in Account.objects.root_nodes()
        ]
        if sum(balances, Balance()) != 0:
            raise exceptions.AccountingEquationViolationError(
                'Account balances do not sum to zero. They sum to {}'.format(
                    sum(balances)))

    def __str__(self):
        name = self.name or 'Unnamed Account'
        if self.is_leaf_node():
            return '{} [{}]'.format(name, self.full_code or '-')
        else:
            return name

    def natural_key(self):
        return (self.uuid, )

    @property
    def sign(self):
        """
        Returns 1 if a credit should increase the value of the
        account, or -1 if a credit should decrease the value of the
        account.

        This is based on the account type as is standard accounting practice.
        The signs can be derrived from the following expanded form of the
        accounting equation:

            Assets = Liabilities + Equity + (Income - Expenses)

        Which can be rearranged as:

            0 = Liabilities + Equity + Income - Expenses - Assets

        Further details here: https://en.wikipedia.org/wiki/Debits_and_credits

        """
        return -1 if self.type in (Account.TYPES.asset,
                                   Account.TYPES.expense) else 1

    def balance(self, as_of=None, raw=False, **kwargs):
        """Get the balance for this account, including child accounts

        Args:
            as_of (Date): Only include transactions on or before this date
            raw (bool): If true the returned balance should not have its sign
                        adjusted for display purposes.
            **kwargs (dict): Will be used to filter the transaction legs

        Returns:
            Balance

        See Also:
            :meth:`simple_balance()`
        """
        balances = [
            account.simple_balance(as_of=as_of, raw=raw, **kwargs)
            for account in self.get_descendants(include_self=True)
        ]
        return sum(balances, Balance())

    def simple_balance(self, as_of=None, raw=False, **kwargs):
        """Get the balance for this account, ignoring all child accounts

        Args:
            as_of (Date): Only include transactions on or before this date
            raw (bool): If true the returned balance should not have its sign
                        adjusted for display purposes.
            **kwargs (dict): Will be used to filter the transaction legs

        Returns:
            Balance
        """
        legs = self.legs
        if as_of:
            legs = legs.filter(transaction__date__lte=as_of)
        if kwargs:
            legs = legs.filter(**kwargs)
        return legs.sum_to_balance() * (1 if raw else
                                        self.sign) + self._zero_balance()

    def _zero_balance(self):
        """Get a balance for this account with all currencies set to zero"""
        return Balance([Money('0', currency) for currency in self.currencies])

    @db_transaction.atomic()
    def transfer_to(self, to_account, amount, **transaction_kwargs):
        """Create a transaction which transfers amount to to_account

        This is a shortcut utility method which simplifies the process of
        transferring between accounts.

        This method attempts to perform the transaction in an intuitive manner.
        For example:

          * Transferring income -> income will result in the former decreasing and the latter increasing
          * Transferring asset (i.e. bank) -> income will result in the balance of both increasing
          * Transferring asset -> asset will result in the former decreasing and the latter increasing

        .. note::

            Transfers in any direction between ``{asset | expense} <-> {income | liability | equity}``
            will always result in both balances increasing. This may change in future if it is
            found to be unhelpful.

            Transfers to trading accounts will always behave as normal.

        Args:

            to_account (Account): The destination account.
            amount (Money): The amount to be transferred.
            transaction_kwargs: Passed through to transaction creation. Useful for setting the
                transaction `description` field.
        """
        if not isinstance(amount, Money):
            raise TypeError('amount must be of type Money')

        if to_account.sign == 1 and to_account.type != self.TYPES.trading:
            # Transferring from two positive-signed accounts implies that
            # the caller wants to reduce the first account and increase the second
            # (which is opposite to the implicit behaviour)
            direction = -1
        else:
            direction = 1

        transaction = Transaction.objects.create(**transaction_kwargs)
        Leg.objects.create(transaction=transaction,
                           account=self,
                           amount=+amount * direction)
        Leg.objects.create(transaction=transaction,
                           account=to_account,
                           amount=-amount * direction)
        return transaction
예제 #19
0
class Folder(MPTTModel):
    TRASH_NAME = u'Trash'
    MEETINGS_NAME = u'Meeting Documents'
    COMMITTEES_NAME = u'Committee Documents'
    MEMBERSHIPS_NAME = u'Member Documents'
    RESERVED_NAMES = (TRASH_NAME, MEETINGS_NAME, COMMITTEES_NAME,
                      MEMBERSHIPS_NAME)
    SPECIAL_FIELDS = Choices(
        # 'member' roles
        (1, 'discussion', _('Discussion')))

    name = models.CharField(_('name'), max_length=255)
    parent = TreeForeignKey('self',
                            verbose_name=_('parent'),
                            related_name='children',
                            null=True,
                            blank=True,
                            on_delete=models.CASCADE)
    account = models.ForeignKey('accounts.Account',
                                verbose_name=_('account'),
                                related_name='folders',
                                null=True,
                                on_delete=models.SET_NULL)
    user = models.ForeignKey('profiles.User',
                             verbose_name=_('user'),
                             related_name='folders',
                             null=True,
                             blank=True,
                             on_delete=models.SET_NULL)
    meeting = models.OneToOneField('meetings.Meeting',
                                   verbose_name=_('meeting'),
                                   blank=True,
                                   null=True,
                                   related_name='folder')
    committee = models.OneToOneField('committees.Committee',
                                     verbose_name=_('committee'),
                                     blank=True,
                                     null=True,
                                     related_name='folder')
    membership = models.OneToOneField('profiles.Membership',
                                      verbose_name=_('membership'),
                                      blank=True,
                                      null=True,
                                      related_name='private_folder')
    slug = models.SlugField(_('slug'), unique=True)
    created = models.DateTimeField(_('created'), auto_now_add=True)
    modified = models.DateTimeField(_('modified'), auto_now=True)
    protected = models.BooleanField(
        _('protected'),
        default=False,
        help_text=_('For special folders like "Trash"'))
    permissions = GenericRelation('permissions.ObjectPermission')

    ordering = models.IntegerField(_('default ordering'),
                                   default=2**10,
                                   null=True,
                                   blank=True)

    special_field = models.PositiveSmallIntegerField(_('special fields'),
                                                     choices=SPECIAL_FIELDS,
                                                     null=True,
                                                     blank=True)
    hidden = models.BooleanField(_('hidden'), default=False)

    objects = FolderManager()

    class MPTTMeta:
        order_insertion_by = ('name', )

    class Meta:
        unique_together = (
            ('parent', 'name'),
            ('account', 'special_field'),
        )
        ordering = ('name', )
        verbose_name = _('folder')
        verbose_name_plural = _('folders')

    def __unicode__(self):
        if self.meeting is not None:
            # Replace meeting id with date in name
            date_str = datefilter(self.meeting.start, 'N j, Y')
            return u'{0} ({1})'.format(self.meeting.name, date_str)
        if self.committee is not None:
            return self.committee.name
        if self.membership is not None:
            return unicode(self.membership)
        return self.name

    def clean(self, *args, **kwargs):
        if self.name and self.name.lower() in [
                n.lower() for n in Folder.RESERVED_NAMES
        ]:
            raise ValidationError(
                _('That folder name is system reserved. Please choose another name.'
                  ))
        super(Folder, self).clean(*args, **kwargs)

    @classmethod
    def generate_slug(cls):
        exists = True
        while exists:
            slug = random_hex(length=20)
            exists = cls.objects.filter(slug=slug).exists()
        return slug

    @classmethod
    def generate_name_from_meeting(cls, meeting):
        id_str = unicode(meeting.id)
        return u'{0} ({1})'.format(meeting.name[:250 - len(id_str)], id_str)

    @classmethod
    def generate_name_from_committee(cls, committee):
        id_str = unicode(committee.id)
        return u'{0} ({1})'.format(committee.name[:250 - len(id_str)], id_str)

    @classmethod
    def generate_name_from_membership(cls, membership):
        id_str = unicode(membership.id)
        return u'{0} ({1})'.format(
            unicode(membership)[:250 - len(id_str)], id_str)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = Folder.generate_slug()
        super(Folder, self).save(*args, **kwargs)

    @property
    def is_account_root(self):
        return self.account is not None and self.account.url == self.name and self.level == 0

    @property
    def can_add_folders(self):
        # account root can add (while protected)
        return self.is_account_root or self.committee is not None or self.membership is not None or not self.protected

    @property
    def can_add_files(self):
        # meeting folder can add files (but no folders)
        return self.can_add_folders or self.meeting is not None

    @property
    def sort_date(self):
        # return date used for sorting
        return self.created if self.protected else self.modified

    def get_absolute_url(self):
        return reverse('folders:folder_detail',
                       kwargs={
                           'slug': self.slug,
                           'url': self.account.url
                       }) if self.account else None

    def get_parents_without_root(self):
        return self.get_ancestors().filter(parent__isnull=False)
예제 #20
0
class Comment(MPTTModel):
    parent = TreeForeignKey('self',
                            on_delete=models.CASCADE,
                            null=True,
                            blank=True,
                            related_name='children',
                            db_index=True)
    post = models.ForeignKey(Post,
                             on_delete=models.CASCADE,
                             null=True,
                             blank=True,
                             related_name='comments')
    page_url = models.CharField(max_length=100, blank=True)
    approved = models.BooleanField(default=False, db_index=True)
    pub_date = models.DateTimeField('date published',
                                    default=timezone.now,
                                    editable=False,
                                    db_index=True)
    author = models.ForeignKey(Commenter,
                               on_delete=models.CASCADE,
                               related_name='comments')
    text = models.TextField(
        blank=True
    )  # form should force this field anyway, so this is just for the admin
    notify = models.BooleanField(default=False)
    spam = models.BooleanField(default=False)
    html_text = models.TextField(
        blank=True
    )  # creating this field to take wordpress imported comments because they're formatted in html
    uuid = models.UUIDField(default=uuid.uuid4, editable=False)

    class Meta:
        ordering = ['pub_date']

    class MPTTMeta:
        order_insertion_by = ['pub_date']

    def __str__(self):
        return str(self.pk)

    def get_absolute_url(self):
        if self.post:
            base_url = self.post.get_absolute_url()
        else:
            base_url = self.page_url
        return base_url + '#comment' + str(self.uuid)

    def get_unsubscribe_url(self):
        if self.post:
            base_url = self.post.get_absolute_url()
        else:
            base_url = self.page_url
        unsubscribe_query = '?email=%s&comment=%s' % (self.author.email,
                                                      str(self.uuid))
        return base_url + unsubscribe_query

    def approve(self):
        self.approved = True
        self.save()

    def unapprove(self):
        self.approved = False
        self.save()

    def get_post_title(self):
        if self.post:
            return self.post.title
        # if post isn't set, it's a page, so try to find it
        try:
            page = FlatPage.objects.get(url=self.page_url)
            return page.title
        except (FlatPage.DoesNotExist, FlatPage.MultipleObjectsReturned):
            logger.error('FlatPage query error: %s' % self.page_url)

    def save(self, *args, **kwargs):  # override save to parse bbcode first
        if self.text:
            parser = get_parser()
            self.html_text = parser.render(self.text)
        super().save(*args, **kwargs)

    # if the email passed in matches the author, then turn off notifications
    def unsubscribe(self, email):
        if email == self.author.email:
            self.notify = False
            self.save()

    def send_email_notification(self, info_dict, recipients):
        if self.author.email in recipients:  # don't send comments to yourself
            recipients.remove(self.author.email)
        if len(recipients) < 1:  # if there are no more recipients, quit out
            return
        subject = "New comment on %s" % self.get_post_title()
        comment_url = info_dict['url']
        context = {
            'comment_author': self.author.username,
            'comment_text': self.text,
            'comment_url': comment_url,
            'unsubscribe_url': info_dict['unsubscribe']
        }
        body = "Check out the reply to your comment at %s" % comment_url
        html_body = render_to_string('comments/comment_body.html', context)
        msg = EmailMultiAlternatives(subject=subject,
                                     from_email="*****@*****.**",
                                     to=recipients,
                                     body=body)
        msg.attach_alternative(html_body, "text/html")
        msg.send()

    def notify_authors(self):
        if not self.notify:
            return []
        recipients = [
            self.author.email
        ]  # first send the notification to the parent comment's author
        return recipients

    def send_notifications(self, info_dict):
        if self.spam_check(
                info_dict):  # don't send notifications for suspected spam
            return
        self.send_email_notification(
            info_dict, ["*****@*****.**"
                        ])  # first always send notification to me, the admin
        if self.parent and self.approved:
            self.send_email_notification(info_dict,
                                         self.parent.notify_authors())

    def spam_check(self, info_dict):
        if self.author.spam:
            return True

        approved = self.author.approved
        if approved:
            return False
        current_domain = Site.objects.get_current().domain
        user_agent = 'Marth Blog/0.0.1'
        akismet = Akismet(settings.AKISMET_KEY,
                          'http://{0}'.format(current_domain), user_agent)
        is_spam = akismet.check(info_dict['remote_addr'],
                                info_dict['user_agent'],
                                comment_author=self.author.username,
                                comment_author_email=self.author.email,
                                comment_author_url=self.author.website,
                                comment_content=self.text)
        if is_spam:
            self.author.mark_spam()
        return is_spam

    def get_request_info(self, request):
        info_dict = {}
        info_dict['url'] = request.build_absolute_uri(self.get_absolute_url())
        info_dict['unsubscribe'] = request.build_absolute_uri(
            self.get_unsubscribe_url())
        info_dict['remote_addr'] = request.META.get('REMOTE_ADDR')
        info_dict['user_agent'] = request.META.get('HTTP_USER_AGENT')
        return info_dict
예제 #21
0
class MenuItem(MPTTModel):

    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children')
    label = models.CharField(
        _('label'),
        max_length=255,
        help_text="The display name on the web site.",
    )
    slug = models.SlugField(
        _('slug'),
        unique=True,
        max_length=255,
        help_text="Unique identifier for this menu item (also CSS ID)")
    order = models.IntegerField(
        _('order'),
        choices=[(x, x) for x in range(0, 51)],
    )
    is_enabled = models.BooleanField(default=True)
    link = models.CharField(
        _('link'),
        max_length=255,
        help_text=
        "The view of the page you want to link to, as a python path or the shortened URL name.",
        blank=True,
    )
    content_type = models.ForeignKey(
        ContentType,
        null=True,
        blank=True,
    )
    object_id = models.CharField(
        # use a CharField to be able to point to tables with UUID pks
        max_length=36,
        blank=True,
        db_index=True,
        default='')
    content_object = fields.GenericForeignKey('content_type', 'object_id')
    href = models.CharField(_('href'), editable=False, max_length=255)

    objects = MenuItemManager()

    class Meta:
        ordering = ('lft', 'tree_id')

    class MPTTMeta:
        order_insertion_by = ('order', )

    def to_tree(self):
        cache_key = 'menu-tree-%s' % self.slug
        root = cache.get(cache_key)
        if not root:
            item = root = Item(self)
            descendents = self.get_descendants()
            for prev, curr, next in previous_current_next(descendents):
                previous_item = item
                item = Item(curr)
                if not prev or prev.level < curr.level:
                    previous_item.add_child(item)
                elif prev and prev.level > curr.level:
                    parent = previous_item
                    while parent.node.level >= curr.level:
                        parent = parent.parent
                    parent.add_child(item)
                else:
                    previous_item.parent.add_child(item)
            cache.set(cache_key, root)
        return root

    def save(self, *args, **kwargs):
        literal_url_prefixes = ('/', 'http://', 'https://')
        regex_url_prefixes = ('^', )
        if self.link:
            if any([self.link.startswith(s) for s in literal_url_prefixes]):
                self.href = self.link
            elif any([self.link.startswith(s) for s in regex_url_prefixes]):
                self.href = ''  # regex should not be used as an actual URL
            else:
                self.href = reverse(self.link)
        elif self.content_object:
            self.href = self.content_object.get_absolute_url()
        else:
            self.href = ''
        delete_cache()
        super(MenuItem, self).save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        delete_cache()
        super(MenuItem, self).delete(*args, **kwargs)

    def __unicode__(self):
        return self.slug
예제 #22
0
class InventoryItem(MPTTModel, ComponentModel):
    """
    An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
    InventoryItems are used only for inventory purposes.
    """

    parent = TreeForeignKey(
        to="self",
        on_delete=models.CASCADE,
        related_name="child_items",
        blank=True,
        null=True,
        db_index=True,
    )
    manufacturer = models.ForeignKey(
        to="dcim.Manufacturer",
        on_delete=models.PROTECT,
        related_name="inventory_items",
        blank=True,
        null=True,
    )
    part_id = models.CharField(
        max_length=50,
        verbose_name="Part ID",
        blank=True,
        help_text="Manufacturer-assigned part identifier",
    )
    serial = models.CharField(max_length=255, verbose_name="Serial number", blank=True, db_index=True)
    asset_tag = models.CharField(
        max_length=50,
        unique=True,
        blank=True,
        null=True,
        verbose_name="Asset tag",
        help_text="A unique tag used to identify this item",
    )
    discovered = models.BooleanField(default=False, help_text="This item was automatically discovered")

    objects = TreeManager()

    csv_headers = [
        "device",
        "name",
        "label",
        "manufacturer",
        "part_id",
        "serial",
        "asset_tag",
        "discovered",
        "description",
    ]

    class Meta:
        ordering = ("device__id", "parent__id", "_name")
        unique_together = ("device", "parent", "name")

    def get_absolute_url(self):
        return reverse("dcim:inventoryitem", kwargs={"pk": self.pk})

    def to_csv(self):
        return (
            self.device.name or "{{{}}}".format(self.device.pk),
            self.name,
            self.label,
            self.manufacturer.name if self.manufacturer else None,
            self.part_id,
            self.serial,
            self.asset_tag,
            self.discovered,
            self.description,
        )
예제 #23
0
class SQLLocation(MPTTModel):
    domain = models.CharField(max_length=255, db_index=True)
    name = models.CharField(max_length=255, null=True)
    location_id = models.CharField(max_length=100, db_index=True, unique=True)
    location_type = models.ForeignKey(LocationType, on_delete=models.CASCADE)
    site_code = models.CharField(max_length=255)
    external_id = models.CharField(max_length=255, null=True, blank=True)
    metadata = jsonfield.JSONField(default=dict, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    last_modified = models.DateTimeField(auto_now=True)
    is_archived = models.BooleanField(default=False)
    latitude = models.DecimalField(max_digits=20, decimal_places=10, null=True, blank=True)
    longitude = models.DecimalField(max_digits=20, decimal_places=10, null=True, blank=True)
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE)

    # Use getter and setter below to access this value
    # since stocks_all_products can cause an empty list to
    # be what is stored for a location that actually has
    # all products available.
    _products = models.ManyToManyField(SQLProduct)
    stocks_all_products = models.BooleanField(default=True)

    supply_point_id = models.CharField(max_length=255, db_index=True, unique=True, null=True, blank=True)

    # For locations where location_type.has_user == True
    user_id = models.CharField(max_length=255, blank=True)

    objects = _tree_manager = LocationManager()
    # This should really be the default location manager
    active_objects = OnlyUnarchivedLocationManager()

    @classmethod
    def get_sync_fields(cls):
        return ["domain", "name", "site_code", "external_id",
                "metadata", "is_archived"]

    @transaction.atomic()
    def save(self, *args, **kwargs):
        from corehq.apps.commtrack.models import sync_supply_point
        from .document_store import publish_location_saved

        if not self.location_id:
            self.location_id = uuid.uuid4().hex
        set_site_code_if_needed(self)
        sync_supply_point(self)
        super(SQLLocation, self).save(*args, **kwargs)
        publish_location_saved(self.domain, self.location_id)

    def delete(self, *args, **kwargs):
        from corehq.apps.commtrack.models import sync_supply_point
        from .document_store import publish_location_saved
        to_delete = self.get_descendants(include_self=True)

        for loc in to_delete:
            loc._remove_users()
            sync_supply_point(loc, is_deletion=True)

        super(SQLLocation, self).delete(*args, **kwargs)
        publish_location_saved(self.domain, self.location_id, is_deletion=True)

    full_delete = delete

    def to_json(self):
        return {
            'name': self.name,
            'site_code': self.site_code,
            '_id': self.location_id,
            'location_id': self.location_id,
            'doc_type': 'Location',
            'domain': self.domain,
            'external_id': self.external_id,
            'is_archived': self.is_archived,
            'last_modified': self.last_modified.isoformat(),
            'latitude': float(self.latitude) if self.latitude else None,
            'longitude': float(self.longitude) if self.longitude else None,
            'metadata': self.metadata,
            'location_type': self.location_type.name,
            'location_type_code': self.location_type.code,
            'lineage': self.lineage,
            'parent_location_id': self.parent_location_id,
        }

    @property
    def lineage(self):
        return list(self.get_ancestors(ascending=True).location_ids())

    _id = property(lambda self: self.location_id)
    get_id = property(lambda self: self.location_id)
    group_id = property(lambda self: self.location_id)

    @property
    def products(self):
        """
        If there are no products specified for this location, assume all
        products for the domain are relevant.
        """
        if self.stocks_all_products:
            return SQLProduct.by_domain(self.domain)
        else:
            return self._products.all()

    @products.setter
    def products(self, value):
        # this will set stocks_all_products to true if the user
        # has added all products in the domain to this location
        self.stocks_all_products = (set(value) ==
                                    set(SQLProduct.by_domain(self.domain)))

        self._products = value

    def _remove_users(self):
        """
        Unassigns the users assigned to that location.

        Used by both archive and delete methods
        """
        if self.user_id:
            from corehq.apps.users.models import CommCareUser
            user = CommCareUser.get(self.user_id)
            user.active = False
            user.save()

        _unassign_users_from_location(self.domain, self.location_id)

    def archive(self):
        """
        Mark a location and its descendants as archived and unassigns users
        assigned to the location.
        """
        for loc in self.get_descendants(include_self=True):
            loc.is_archived = True
            loc.save()
            loc._remove_users()

    def unarchive(self):
        """
        Unarchive a location and reopen supply point case if it
        exists.
        """
        for loc in self.get_descendants(include_self=True):
            loc.is_archived = False
            loc.save()

            if loc.user_id:
                from corehq.apps.users.models import CommCareUser
                user = CommCareUser.get(loc.user_id)
                user.active = True
                user.save()

    class Meta:
        app_label = 'locations'
        unique_together = ('domain', 'site_code',)
        index_together = [
            ('tree_id', 'lft', 'rght')
        ]

    def __unicode__(self):
        return u"{} ({})".format(self.name, self.domain)

    def __repr__(self):
        return u"SQLLocation(domain='{}', name='{}', location_type='{}')".format(
            self.domain,
            self.name,
            self.location_type.name if hasattr(self, 'location_type') else None,
        ).encode('utf-8')

    @property
    def display_name(self):
        return u"{} [{}]".format(self.name, self.location_type.name)

    def archived_descendants(self):
        """
        Returns a list of archived descendants for this location.
        """
        return self.get_descendants().filter(is_archived=True)

    def child_locations(self, include_archive_ancestors=False):
        """
        Returns a list of this location's children.
        """
        children = self.get_children()
        return filter_for_archived(children, include_archive_ancestors)

    @classmethod
    def root_locations(cls, domain, include_archive_ancestors=False):
        roots = cls.objects.root_nodes().filter(domain=domain)
        return filter_for_archived(roots, include_archive_ancestors)

    def get_path_display(self):
        return '/'.join(self.get_ancestors(include_self=True)
                            .values_list('name', flat=True))

    def get_case_sharing_groups(self, for_user_id=None):
        if self.location_type.shares_cases:
            yield self.case_sharing_group_object(for_user_id)
        if self.location_type.view_descendants:
            for sql_loc in self.get_descendants().filter(location_type__shares_cases=True, is_archived=False):
                yield sql_loc.case_sharing_group_object(for_user_id)

    def case_sharing_group_object(self, user_id=None):
        """
        Returns a fake group object that cannot be saved.

        This is used for giving users access via case sharing groups, without
        having a real group for every location that we have to manage/hide.
        """
        from corehq.apps.groups.models import UnsavableGroup

        group = UnsavableGroup(
            domain=self.domain,
            users=[user_id] if user_id else [],
            last_modified=datetime.utcnow(),
            name=self.get_path_display() + '-Cases',
            _id=self.location_id,
            case_sharing=True,
            reporting=False,
            metadata={
                'commcare_location_type': self.location_type.name,
                'commcare_location_name': self.name,
            },
        )

        for key, val in self.metadata.items():
            group.metadata['commcare_location_' + key] = val

        return group

    def is_direct_ancestor_of(self, location):
        return (location.get_ancestors(include_self=True)
                .filter(pk=self.pk).exists())

    @classmethod
    def by_domain(cls, domain):
        return cls.objects.filter(domain=domain)

    @property
    def path(self):
        _path = list(reversed(self.lineage))
        _path.append(self._id)
        return _path

    @classmethod
    def by_location_id(cls, location_id):
        try:
            return cls.objects.get(location_id=location_id)
        except cls.DoesNotExist:
            return None

    # For quick_find compatability
    by_id = by_location_id

    def linked_supply_point(self):
        if not self.supply_point_id:
            return None
        try:
            return SupplyInterface(self.domain).get_supply_point(self.supply_point_id)
        except CaseNotFound:
            return None

    @property
    def parent_location_id(self):
        return self.parent.location_id if self.parent else None

    @property
    def location_type_object(self):
        return self.location_type

    @property
    def location_type_name(self):
        return self.location_type.name

    @property
    def sql_location(self):
        # For backwards compatability
        return self
예제 #24
0
class Comment(MttpContentTypeAware):
    author_name = models.CharField(null=False, max_length=12)
    author = models.ForeignKey('users.ScUser')
    submission = models.ForeignKey(Submission)
    parent = TreeForeignKey('self',
                            related_name='children',
                            null=True,
                            blank=True,
                            db_index=True)
    timestamp = models.DateTimeField(default=timezone.now())
    ups = models.IntegerField(default=0)
    downs = models.IntegerField(default=0)
    score = models.IntegerField(default=0)
    raw_comment = models.TextField(blank=True)
    html_comment = models.TextField(blank=True)
    markedBySubmissionOwner = models.BooleanField(default=False)
    url = models.CharField(null=True, blank=True, max_length=1000)
    ltp = models.IntegerField(default=0)
    image = models.ImageField(upload_to='comments/', null=True)

    class MPTTMeta:
        order_insertion_by = ['-score']

    @classmethod
    def create(cls, author, raw_comment, parent, ltp, link, image):
        """
        Create a new comment instance. If the parent is submisison
        update comment_count field and save it.
        If parent is comment post it as child comment
        :param author: RedditUser instance
        :type author: RedditUser
        :param raw_comment: Raw comment text
        :type raw_comment: str
        :param parent: Comment or Submission that this comment is child of
        :type parent: Comment | Submission
        :return: New Comment instance
        :rtype: Comment
        """

        html_comment = mistune.markdown(raw_comment)
        # todo: any exceptions possible?
        comment = cls(author=author,
                      author_name=author.user.username,
                      raw_comment=raw_comment,
                      html_comment=html_comment,
                      ltp=ltp,
                      url=link,
                      image=image)

        if isinstance(parent, Submission):
            submission = parent
            comment.submission = submission
        elif isinstance(parent, Comment):
            submission = parent.submission
            comment.submission = submission
            comment.parent = parent
        else:
            return
        submission.comment_count += 1
        submission.save()
        comment.timestamp = timezone.now()
        return comment

    def __unicode__(self):
        return "<Comment:{}>".format(self.id)
예제 #25
0
class Category(MPTTModel, Slugify):
    slug = CharField(max_length=255,
                     null=True,
                     verbose_name='URL',
                     unique=True)
    description = ManyToManyField(CategoryDescription, related_name="obj")
    parent = TreeForeignKey('self',
                            blank=True,
                            null=True,
                            verbose_name="Родитель",
                            related_name="child",
                            on_delete=CASCADE)
    image = ImageField(upload_to='data/category/',
                       blank=True,
                       null=True,
                       verbose_name='Картинка')
    last_modified = DateTimeField(auto_now_add=True)
    active = BooleanField(default=1, verbose_name='Активна')
    bgcolor = CharField(max_length=20, null=True)

    def cache(self):
        super().cache()

        for device in ['mobile', 'desktop']:
            for lang in Language.objects.all():
                pattern = '{CACHE_URL}cache/html/{device}/{lang}/static/categories.html'.format(
                    device=device, lang=lang, CACHE_URL=CACHE_URL)
                if os.path.isfile(pattern):
                    os.remove(pattern)

            proc = Popen(['/home/ckl/shop/ffs.sh'],
                         stdin=PIPE,
                         stdout=PIPE,
                         stderr=PIPE)
            output, error = proc.communicate()
            with open('process.log', 'wb') as f:
                f.write(output + error)

    def menu_thumb(self, size=70):
        try:
            path = MEDIA_ROOT + urllib.parse.unquote(self.image.url).replace(
                '/media/', '')
        except ValueError:
            return None
        img = Image.open(path)
        img.thumbnail([size, size])

        return img

    @property
    def icon(self):
        return self.image_url(size=100)

    @property
    def productsCount(self):
        count = self.products.count()
        for child in self.child.all():
            count += child.products.count()

        return count

    def save(self, *args, **kwargs):
        if self.active:
            for thumb in self.thumb.all():
                thumb.delete()

        if self.id:
            for product in self.products.all():
                product.cache()

        for lang in Language.objects.all():
            for device in ['desktop', 'mobile']:
                catfile = '{cache}cache/html/{device}/{lang}/static/categories.html'.format(
                    cache=CACHE_URL, device=device, lang=lang.code)
                if os.path.isfile(catfile):
                    os.remove(catfile)

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

    def image_url(self, size=230):
        try:
            image = Category_Thumb.objects.get(category_id=self.pk, size=size)
        except Category_Thumb.MultipleObjectsReturned:
            image = Category_Thumb.objects.filter(category_id=self.pk,
                                                  size=size).first()
        except:
            image = None

        if image:
            if os.path.isfile(CACHE_URL + image.url):
                return image.url
            else:
                image.delete()

        try:
            return self.thumbnail(size)
        except:
            pass

        return '/media/data/no_image_new.jpg'

    def thumbnail(self, size):
        image = Image.open(self.image)
        image.thumbnail([size, size])

        path = self.path(size)

        try:
            image = image.convert('RGBA')
            image.save(CACHE_URL + path, 'PNG')
        except:
            image = image.convert('RGB')
            image.save(CACHE_URL + path, 'JPEG')

        thumb = Category_Thumb.objects.create(url='/' + path,
                                              category=self,
                                              size=size)
        return thumb.url

    def path(self, size):
        name = sub("[^a-zA-Z0-9А-Яа-я]", "", self.image.name.split('/')[-1])
        path = 'images/%s/%s%s/' % (name, size, name[1])
        root = CACHE_URL + path
        if not os.path.isdir(root):
            try:
                os.makedirs(root)
            except FileExistsError:
                pass
        for i in range(0, 32):
            path += choice(ascii_letters)
        return path + '.jpg'

    def similars(self):
        return Category.objects.filter(parent=self.parent).exclude(id=self.id)

    def get_ancestorsf(self, parent, categories):
        if parent.parent and parent.parent.parent:
            categories += (parent.parent, )
            return self.get_ancestorsf(parent.parent, categories)

        return reversed(categories)

    def ancestors(self):
        categories = ()

        if self.parent and self.parent.parent:
            categories += (self.parent, )
            return self.get_ancestorsf(self.parent, categories)

        return reversed(categories)

    def get_root(self, parent):
        if parent.parent:
            return self.get_root(parent.parent)

        return parent

    @property
    def root(self):
        if self.parent:
            return self.get_root(self.parent)
        else:
            return _('Категории товаров')

    def ancestors_breadcrumbs(self, parent, breadcrumbs, lang):
        breadcrumbs = breadcrumbs + (
            (parent.names(lang), parent.slugy(lang)), )
        if parent.parent:
            return self.ancestors_breadcrumbs(parent.parent, breadcrumbs, lang)
        else:
            return breadcrumbs

    def breadcrumbs(self, lang, product=False):
        if self.parent is None:
            if product:
                return ((self.names(lang), self.slugy(lang)), )
            else:
                return
        else:
            if product:
                breadcrumbs = ((self.names(lang), self.slugy(lang)), )
                breadcrumbs = self.ancestors_breadcrumbs(
                    self.parent, breadcrumbs, lang)
            else:
                breadcrumbs = self.ancestors_breadcrumbs(self.parent, (), lang)
            return reversed(breadcrumbs)

    class Meta:
        unique_together = ('slug', 'parent')
        verbose_name = 'Категории'
        verbose_name_plural = 'Категории'
예제 #26
0
class Collection(ObjectPermissionMixin, TagStringMixin, MPTTModel):
    name = models.CharField(max_length=255)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            on_delete=models.CASCADE)
    owner = models.ForeignKey('auth.User',
                              related_name='owned_collections',
                              on_delete=models.CASCADE)
    editors_can_change_permissions = models.BooleanField(default=True)
    discoverable_when_public = models.BooleanField(default=False)
    uid = KpiUidField(uid_prefix='c')
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    objects = CollectionManager()
    tags = TaggableManager(manager=KpiTaggableManager)
    permissions = GenericRelation(ObjectPermission)

    @property
    def kind(self):
        return 'collection'

    class Meta:
        ordering = ('-date_modified', )
        permissions = (
            # change_, add_, and delete_collection are provided automatically
            # by Django
            (PERM_VIEW_COLLECTION, 'Can view collection'),
            (PERM_SHARE_COLLECTION,
             "Can change this collection's sharing settings"),
        )

        # Since Django 2.1, 4 permissions are added for each registered model:
        # - add
        # - change
        # - delete
        # - view
        # See https://docs.djangoproject.com/en/2.2/topics/auth/default/#default-permissions
        # for more detail.
        # `view_collection` clashes with newly built-in one.
        # The simplest way to fix this is to keep old behaviour
        default_permissions = ('add', 'change', 'delete')

    # Assignable permissions that are stored in the database
    ASSIGNABLE_PERMISSIONS = (PERM_VIEW_COLLECTION, PERM_CHANGE_COLLECTION)
    # Calculated permissions that are neither directly assignable nor stored
    # in the database, but instead implied by assignable permissions
    CALCULATED_PERMISSIONS = (PERM_SHARE_COLLECTION, PERM_DELETE_COLLECTION)
    # Granting some permissions implies also granting other permissions
    IMPLIED_PERMISSIONS = {
        # Format: explicit: (implied, implied, ...)
        PERM_CHANGE_COLLECTION: (PERM_VIEW_COLLECTION, ),
    }

    def get_ancestors_or_none(self):
        # ancestors are ordered from farthest to nearest
        ancestors = self.get_ancestors()
        if ancestors.exists():
            return ancestors
        else:
            return None

    def get_mixed_children(self):
        """ Returns all children, both Assets and Collections """
        return CollectionChildrenQuerySet(self)

    def __str__(self):
        return self.name
예제 #27
0
class Region(MPTTModel):
    # objects = RegionManager()

    code = models.CharField(max_length=50, unique=True)
    name = models.CharField(max_length=255)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children')

    # Save bbox values in the database.
    # This is useful for spatial searches and for generating thumbnail images
    # and metadata records.
    bbox_x0 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    bbox_x1 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    bbox_y0 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    bbox_y1 = models.DecimalField(max_digits=19,
                                  decimal_places=10,
                                  blank=True,
                                  null=True)
    srid = models.CharField(max_length=255, default='EPSG:4326')

    def __unicode__(self):
        return self.name

    @property
    def bbox(self):
        return [
            self.bbox_x0, self.bbox_x1, self.bbox_y0, self.bbox_y1, self.srid
        ]

    @property
    def bbox_string(self):
        return ",".join([
            str(self.bbox_x0),
            str(self.bbox_y0),
            str(self.bbox_x1),
            str(self.bbox_y1)
        ])

    @property
    def geographic_bounding_box(self):
        return bbox_to_wkt(self.bbox_x0,
                           self.bbox_x1,
                           self.bbox_y0,
                           self.bbox_y1,
                           srid=self.srid)

    class Meta:
        ordering = ("name", )
        verbose_name_plural = 'Metadata Regions'

    class MPTTMeta:
        order_insertion_by = ['name']
예제 #28
0
class Product(BaseModel):
    title = models.CharField(max_length=255, verbose_name="Заголовок")
    manufacturer = models.ForeignKey(Manufacturer,
                                     blank=True,
                                     null=True,
                                     on_delete=None,
                                     related_name="catalog_manufacturer")
    category = TreeForeignKey(Category,
                              blank=True,
                              null=True,
                              verbose_name='Категория',
                              on_delete=models.CASCADE)
    category_rozetka = TreeForeignKey(RozetkaCategory,
                                      blank=True,
                                      null=True,
                                      verbose_name='Категория розетка',
                                      on_delete=None)
    import_to_rozetka = models.BooleanField(verbose_name='На розетку',
                                            default=False)
    import_to_prom = models.BooleanField(verbose_name='На PROM', default=False)
    count_in_package = models.PositiveSmallIntegerField(
        verbose_name="Кол-во в упаковке", default=1)
    price = models.DecimalField(verbose_name="Цена",
                                max_digits=8,
                                decimal_places=2,
                                blank=True,
                                null=True)
    old_price_percent = models.DecimalField(
        verbose_name="Наценка в процентах для старай цены",
        max_digits=5,
        decimal_places=2,
        blank=True,
        null=True)
    promo_percent = models.DecimalField(
        verbose_name="Процент скидки для промо",
        max_digits=5,
        decimal_places=2,
        blank=True,
        null=True)
    discont = models.DecimalField(verbose_name='Скидка',
                                  decimal_places=2,
                                  max_digits=4,
                                  blank=True,
                                  null=True)
    stock_quantity = models.PositiveSmallIntegerField(default=100,
                                                      verbose_name='Остаток')
    availability_prom = models.CharField(
        verbose_name='Наличие товара для прома',
        max_length=3,
        help_text=availability_prom_help_text,
        default='+',
        blank=True,
    )
    currency = models.ForeignKey(Currency,
                                 null=True,
                                 blank=True,
                                 default=None,
                                 on_delete=models.CASCADE)
    course = models.DecimalField(verbose_name='Курс',
                                 max_digits=12,
                                 decimal_places=5,
                                 blank=True,
                                 null=True,
                                 default=1)
    re_count = models.BooleanField(verbose_name="Пересчитывать в грн?",
                                   default=True)
    unit = models.ForeignKey(Unit,
                             verbose_name='Единица измерения',
                             blank=True,
                             null=True,
                             default=None,
                             on_delete=models.CASCADE)
    step = models.DecimalField(verbose_name="Шаг",
                               max_digits=8,
                               decimal_places=3,
                               default=1)
    text = RichTextUploadingField(verbose_name="Текст поста",
                                  blank=True,
                                  default="")
    image = models.ImageField(verbose_name="Изображение",
                              blank=True,
                              default='',
                              upload_to=set_image_name)
    active = models.BooleanField(default=True, verbose_name="Вкл/Выкл")
    code = models.CharField(verbose_name="Артикул",
                            max_length=20,
                            default=set_code,
                            unique=True,
                            blank=True,
                            null=True)

    vendor_id = models.CharField(blank=True, null=True, max_length=50)
    vendor_name = models.CharField(blank=True, null=True, max_length=200)

    author = models.ForeignKey(User,
                               on_delete=models.SET_NULL,
                               blank=True,
                               null=True)
    is_checked = models.BooleanField(default=False)

    class Meta:
        verbose_name = "Товар"
        verbose_name_plural = "Товары"
        ordering = ('-code', )
        permissions = (
            ('can_update_mizol', 'Может обновлять прайсы Мизол'),
            ('can_update_prom_parameters',
             'Может обновлять параметры с прома'),
            ('Freelanser', 'freelanser'),
        )

    def __str__(self):
        return "{}".format(self.title)

    def save(self, *args, **kwargs):
        if not self.author:
            try:
                user = get_current_user()
                self.author = user
            except:
                self.user = None

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

    def get_absolute_url(self):
        return reverse('single-product', args=[str(self.id)])

    def import_for_admin(self):
        styles = "color: #fff; border-radius: 2px; padding: 3px 7px; min-width: 50px; display: block; text-align: center;"
        if self.import_to_rozetka:
            return mark_safe(
                "<span style='background: green;{}'>Rozetka</span>".format(
                    styles))
        elif self.import_to_prom:
            return mark_safe(
                "<span style='background: linear-gradient(135deg,#4854a2,#772088);{}'>Prom</span>"
                .format(styles))

    import_for_admin.short_description = 'Импорт'

    def get_currency_code(self):
        if self.currency:
            if self.re_count:
                return Currency.objects.get(code='UAH').code
            return self.currency.code
        return None

    get_currency_code.short_description = 'Валюта'

    def get_price(self):
        if not self.price:
            return None
        if self.discont:
            return self.price - (self.price * self.discont) / 100
        return self.price

    def get_price_UAH(self):
        price = self.get_price()
        if price:
            if self.re_count:
                return round(price * self.course * self.count_in_package, 3)
            else:
                return round(price * self.count_in_package, 3)
        return False

    get_price_UAH.short_description = 'Цена в валюте'

    def get_promo_price(self):
        price = self.get_price_UAH()
        if price and self.promo_percent:
            return round(price - (price * self.promo_percent) / 100, 2)
        return False

    get_promo_price.short_description = 'Цена промо'

    def get_old_price(self):
        price_uah = self.get_price_UAH()
        old_price = (price_uah * self.old_price_percent) / 100 + price_uah
        return round(old_price, 2)

    def get_delivery_count(self):
        return self.delivery_set.count()

    def get_unit(self):
        if self.unit:
            return self.unit.short_title
        return 'шт.'

    def get_images_count(self):
        return Photo.objects.filter(product=self).count()

    get_images_count.short_description = 'Доп изобр.'

    def get_all_photo(self):
        images = list()
        other_photo = Photo.objects.filter(product=self)

        if self.image:
            images.append(self.image.url)

        for item in other_photo:
            images.append(item.image.url)

        return images

    def get_images(self):
        return Photo.objects.filter(product=self)
예제 #29
0
class AdminBoundary(MPTTModel, models.Model):
    """
    Represents a single administrative boundary (like a country, state or district)
    """
    osm_id = models.CharField(
        max_length=15,
        unique=True,
        help_text="This is the OSM id for this administrative boundary")

    name = models.CharField(
        max_length=128, help_text="The name of our administrative boundary")

    level = models.IntegerField(
        help_text=
        "The level of the boundary, 0 for country, 1 for state, 2 for district, 3 for ward"
    )

    parent = TreeForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='children',
        db_index=True,
        help_text="The parent to this political boundary if any")

    geometry = models.MultiPolygonField(
        null=True,
        help_text="The full geometry of this administrative boundary")

    simplified_geometry = models.MultiPolygonField(
        null=True,
        help_text="The simplified geometry of this administrative boundary")

    objects = models.GeoManager()

    @staticmethod
    def get_geojson_dump(features):
        # build a feature collection
        feature_collection = geojson.FeatureCollection(features)
        return geojson.dumps(feature_collection)

    def as_json(self):
        result = dict(osm_id=self.osm_id,
                      name=self.name,
                      level=self.level,
                      aliases='')

        if self.parent:
            result['parent_osm_id'] = self.parent.osm_id

        aliases = '\n'.join([alias.name for alias in self.aliases.all()])
        result['aliases'] = aliases
        return result

    def get_geojson_feature(self):
        return geojson.Feature(
            properties=dict(name=self.name,
                            osm_id=self.osm_id,
                            id=self.pk,
                            level=self.level),
            zoomable=True if self.children.all() else False,
            geometry=None if not self.simplified_geometry else geojson.loads(
                self.simplified_geometry.geojson))

    def get_geojson(self):
        return AdminBoundary.get_geojson_dump([self.get_geojson_feature()])

    def get_children_geojson(self):
        children = []
        for child in self.children.all():
            children.append(child.get_geojson_feature())
        return AdminBoundary.get_geojson_dump(children)

    def update(self, **kwargs):
        AdminBoundary.objects.filter(id=self.id).update(**kwargs)

        # if our name changed, update the category on any of our values
        name = kwargs.get('name', self.name)
        if name != self.name:
            from temba.values.models import Value
            Value.objects.filter(location_value=self).update(category=name)

        # update our object values so that self is up to date
        for key, value in kwargs.items():
            setattr(self, key, value)

    def __unicode__(self):
        return "%s" % self.name
예제 #30
0
class CategoryCD(MPTTModel):
    parent = TreeForeignKey('self',
                            verbose_name=_("parent"),
                            blank=True,
                            null=True,
                            related_name='children_category')
    title = models.CharField(max_length=500, verbose_name=_('title'))
    slug = models.CharField(max_length=500, verbose_name=_('slug'), blank=True)
    text = TinymceField.HTMLField(
        max_length=10000,
        verbose_name=_('text'),
        blank=True,
        help_text=_('description category. not show'))
    image = SorlImageField(max_length=500,
                           upload_to='upload/categorycd/',
                           verbose_name=_('image'),
                           blank=True,
                           null=True)

    is_active = models.BooleanField(verbose_name=_('is active'), default=True)
    sort = models.IntegerField(verbose_name=_('sort'), default=0)

    def __unicode__(self):
        return self.title

    class Meta:
        verbose_name = _('category cd')
        verbose_name_plural = _('categorys cd')
        ordering = ['sort']

    class MPTTMeta:
        parent_attr = 'parent'
        order_insertion_by = ['sort']

    def get_breadcrumbs(self):
        return list(
            self.get_ancestors(ascending=False,
                               include_self=True).filter(is_active=True))

    #Возвращает имя
    def get_title(self):
        return self.title

    #Возвращает картинку
    def get_image(self):
        if self.image:
            return self.image
        return ''

    #Возвращает подкатегории
    def get_subcategory(self):
        return self.get_children().filter(is_active=True)

    def small_image(self):
        if self.image:
            f = get_thumbnail(self.image,
                              '80x60',
                              crop='center',
                              quality=99,
                              format='PNG')
            html = '<a href="%s"><img src="%s" title="%s" /></a>'
            return html % (self.image.url, f.url, self.title)
        return u'<img src="/media/img/no_image_min.png" title="%s" />' % self.title

    small_image.short_description = _("Image")
    small_image.allow_tags = True

    def get_all_products(self):
        return self.cds_category.all()

    def clean(self):
        r = re.compile('^([a-zA-Z0-9_-]+)\.(jpg|jpeg|png|bmp|gif)$',
                       re.IGNORECASE)
        if self.image:
            if not r.findall(os.path.split(self.image.url)[1]):
                raise ValidationError(_("File name validation error."))

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        super(CategoryCD, self).save(*args, **kwargs)