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']
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')
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})
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)
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
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']
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()
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']
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)
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, )
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
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)
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()
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()
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)
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
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'
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
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)
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
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
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, )
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
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)
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 = 'Категории'
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
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']
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)
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
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)