예제 #1
0
class Event(MPTTModel, BaseModel, SchemalessFieldMixin):
    """
        Tavastia Events
        publisher muutettu vapaaehtoiseksi
        lisätty PIN-koodi, vain näkyvä CharField kenttä. Tarvetta salatulle salasanakentälle?
    """
    jsonld_type = "Event/LinkedEvent"
    """
    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')),
        # Other types include e.g. a festival
        (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,
                                blank=True)
    secondary_headline = models.CharField(verbose_name=_('Secondary headline'),
                                          max_length=255,
                                          null=True,
                                          db_index=True,
                                          blank=True)
    provider = models.CharField(verbose_name=_('Provider'),
                                max_length=512,
                                null=True)
    publisher = models.ForeignKey('django_orghierarchy.Organization',
                                  verbose_name=_('Publisher'),
                                  db_index=True,
                                  on_delete=models.PROTECT,
                                  related_name='published_events',
                                  null=True,
                                  blank=True)

    # 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)

    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,
                                        default=None,
                                        choices=SUPER_EVENT_TYPES,
                                        db_index=True)

    in_language = models.ManyToManyField(Language,
                                         verbose_name=_('In language'),
                                         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)
    """
        Tavastia Events
        PIN-koodi, tapahtuman esteettömyys ja järjestäjän sähköpostiosoite lisätty kentiksi
        PIN-koodi ja esteettömyys ovat pakollisia kenttiä
        PIN-koodia ja järjestäjän sähköpostiosoitetta ei näytetä tapahtumien haussa
    """
    pin = models.CharField(blank=False, max_length=64, default='0000')
    accessible = models.BooleanField(default=False, null=False, blank=False)
    provider_email = models.EmailField(null=True,
                                       blank=True,
                                       default='*****@*****.**')

    multi_day = models.BooleanField(default=False, null=False)

    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)
        """
            AvoinHäme
            Paikan tapahtumien päivittäminen muokkauksen yhteydessä
        """
        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)
            call_command('update_n_events')

    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)

    def filter_deleted(self):
        return self.sub_events.filter(deleted=False)
예제 #2
0
class Compte(MPTTModel):
    nom = models.CharField(max_length=100)
    parent = TreeForeignKey('self',
                            on_delete=models.CASCADE,
                            null=True,
                            blank=True,
                            related_name='sous_comptes')

    gestionnaires = models.ManyToManyField('User', blank=True)

    CATEGORIE_DEPENSES = 0
    CATEGORIE_ACTIFS = 1
    CATEGORIE_REVENUS = 2
    CATEGORIE_DETTES = 3
    CATEGORIE_FONDS_PROPRES = 4
    CATEGORIE_CHOICES = (
        (CATEGORIE_DEPENSES, "compte de dépenses"),
        (CATEGORIE_ACTIFS, "compte d'actifs"),
        (CATEGORIE_REVENUS, "compte de revenus"),
        (CATEGORIE_DETTES, "compte de dettes"),
        (CATEGORIE_FONDS_PROPRES, "compte de fonds propres"),
    )
    categorie = models.SmallIntegerField(verbose_name="type de compte",
                                         choices=CATEGORIE_CHOICES)

    decouvert_autorise = models.BooleanField(
        default=False,
        verbose_name="découvert autorisé",
        help_text="Ce champ indique si le compte peut être à "
        "découvert. Dans ce cas, on peut limiter le nombre "
        "d'heures du découvert en donnant des valeurs explicites "
        "aux durées à découvert autorisées.")

    decouvert_duree = models.DurationField(
        blank=True,
        null=True,
        verbose_name="durée à découvert autorisée",
        help_text="Lorsque le découvert est autorisé sur ce "
        "compte, ce champ donne une limite sur le nombre d'heures "
        "comptabilisées négativement. Par exemple, si ce champ "
        "vaut 3h, le solde du compte devra toujours être supérieur "
        "ou égal à -3h.")

    decouvert_duree_interrogation = models.DurationField(
        blank=True,
        null=True,
        verbose_name="durée d'interrogation à découvert autorisée")

    def __str__(self):
        return self.nom

    def solde(self, annee=None):
        """
		Calcul du solde du compte pour l'année donnée en paramètre.

		Cette méthode renvoie une liste de dictionnaires, chaque
		dictionnaire donne la durée totale et la durée d'interrogation
		pour un taux donné.
		"""
        if annee is None:
            from pykol.models.base import Annee
            annee = Annee.objects.get_actuelle()

        comptes = self.get_descendants(include_self=True)

        return MouvementLigne.objects.filter(
            compte__in=comptes,
            mouvement__annee=annee,
            mouvement__etat=Mouvement.ETAT_VALIDE).values('taux').aggregate(
                duree=models.Sum('duree'),
                duree_interrogation=models.Sum('duree_interrogation'))

    def sens_affichage(self):
        """
		Renvoie -1 ou 1 pour indiquer si, étant donné sa catégorie, le
		compte doit normalement avoir un solde négatif (notamment pour
		les comptes de revenus) ou un solde positif.
		"""
        if self.categorie in (CATEGORIE_REVENUS, CATEGORIE_DETTES):
            return -1
        else:
            return 1

    def format_duree(self, duree, sens=True):
        """
		Formate une durée en prenant en compte le sens d'affichage du
		compte.
		"""
        if sens:
            return "{:.2f}".format(self.sens_affichage() *
                                   duree.total_seconds() / 3600)
        else:
            return "{:.2f}".format(duree.total_seconds() / 3600)

    def retrait_possible(self, ligne):
        """
		Renvoie True quand le retrait de la ligne donnée en paramètre ne
		provoque pas un dépassement du découvert autorisé.
		"""
        solde = self.solde(ligne.mouvement.annee)

        if self.decouvert_autorise:
            if self.decouvert_duree is None:
                duree_minimale = None
            else:
                duree_minimale = -self.decouvert_duree

            if self.decouvert_duree_interrogation is None:
                duree_interrogation_minimale = None
            else:
                duree_interrogation_minimale = -self.decouvert_duree_interrogation
        else:
            duree_minimale = timedelta()
            duree_interrogation_minimale = timedelta()

        return \
         (duree_minimale is None or
          solde['duree'] - ligne.duree >= duree_minimale) and \
         (duree_interrogation_minimale is None or
          solde['duree_interrogation'] - ligne.duree_interrogation >= \
          duree_interrogation_minimale)
예제 #3
0
class Organizations(MPTTModel):
    name = models.CharField('组织机构名称', max_length=300, null=True)
    attribute = models.CharField('组织机构性质',
                                 max_length=300,
                                 null=True,
                                 blank=True)
    register_date = models.CharField('注册日期',
                                     max_length=30,
                                     null=True,
                                     blank=True)
    owner_name = models.CharField('负责人', max_length=300, null=True, blank=True)
    phone_number = models.CharField('电话号码',
                                    max_length=300,
                                    null=True,
                                    blank=True)
    firm_address = models.CharField('地址',
                                    max_length=300,
                                    null=True,
                                    blank=True)

    # 组织级别
    organlevel = models.CharField('Level',
                                  max_length=30,
                                  null=True,
                                  blank=True)
    # add new
    coorType = models.CharField(max_length=30, null=True, blank=True)
    longitude = models.CharField(max_length=30, null=True, blank=True)
    latitude = models.CharField(max_length=30, null=True, blank=True)
    zoomIn = models.CharField(max_length=30, null=True, blank=True)
    islocation = models.CharField(max_length=30, null=True, blank=True)
    location = models.CharField(max_length=30, null=True, blank=True)
    province = models.CharField(max_length=30, null=True, blank=True)
    city = models.CharField(max_length=30, null=True, blank=True)
    district = models.CharField(max_length=30, null=True, blank=True)

    cid = models.CharField(max_length=300, null=True, blank=True)
    pId = models.CharField(max_length=300, null=True, blank=True)
    is_org = models.BooleanField(max_length=300, blank=True)
    uuid = models.CharField(max_length=300, null=True, blank=True)

    adcode = models.CharField(max_length=300, null=True, blank=True)  #行政代码
    districtlevel = models.CharField(max_length=300, null=True,
                                     blank=True)  #行政级别

    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            on_delete=models.CASCADE,
                            related_name='children',
                            db_index=True)

    class MPTTMeta:
        order_insertion_by = ['name']

    class Meta:
        managed = True
        db_table = 'organizations'

        permissions = (

            # 企业管理 sub
            ('firmmanager', '企业管理'),
            ('rolemanager_firmmanager', '角色管理'),
            ('rolemanager_firmmanager_edit', '角色管理_可写'),
            ('organusermanager_firmmanager', '组织和用户管理'),
            ('organusermanager_firmmanager_edit', '组织和用户管理_可写'),
        )

    def __unicode__(self):
        return self.name

    def __str__(self):
        return self.name

    def sub_organizations(self, include_self=False):
        return self.get_descendants(include_self)

    def sub_stations(self, include_self=False):
        return self.get_descendants(include_self)
예제 #4
0
class Location(MPTTModel, BaseModel):
    name = models.CharField(max_length=50)
    type = models.ForeignKey(LocationType, related_name='locations')
    code = models.CharField(max_length=100, null=True, blank=True)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='sub_locations',
                            db_index=True)
    coordinates = models.ManyToManyField(
        Point, related_name='admin_div_locations'
    )  #would use this in the future. But ignore for now
    tree = TreeManager()
    objects = models.Manager()

    class MPTTMeta:
        order_insertion_by = ['name']
        ordering = [
            'name',
        ]

    def __unicode__(self):
        return self.name

    @property
    def tree_parent(self):
        return self.parent

    def is_sub_location(self, location):
        return location.is_ancestor_of(self)

    class Meta:
        app_label = 'survey'
        # unique_together = [('code', 'type'), ]


#
# class LocationCode(BaseModel):
#     location = models.ForeignKey(Location, null=False, related_name="code")
#     code = models.CharField(max_length=10, null=False, default=0)
#
#     @classmethod
#     def get_household_code(cls, interviewer):
#         location_hierarchy = interviewer.locations_in_hierarchy()
#         codes = cls.objects.filter(location__in=location_hierarchy).order_by('location').values_list('code', flat=True)
#         return ''.join(codes)
#
#
# class LocationAutoComplete(models.Model):
#     location = models.ForeignKey(Location, null=True)
#     text = models.CharField(max_length=500)
#
#     class Meta:
#         app_label = 'survey'
#
#
# def generate_auto_complete_text_for_location(location):
#     auto_complete = LocationAutoComplete.objects.filter(location=location)
#     if not auto_complete:
#         auto_complete = LocationAutoComplete(location=location)
#     else:
#         auto_complete = auto_complete[0]
#     parents = [location.name]
#     while location.tree_parent:
#         location = location.tree_parent
#         parents.append(location.name)
#     parents.reverse()
#     auto_complete.text = " > ".join(parents)
#     auto_complete.save()
#
#
# @receiver(post_save, sender=Location)
# def create_location_auto_complete_text(sender, instance, **kwargs):
#     generate_auto_complete_text_for_location(instance)
#     for location in instance.get_descendants():
#         generate_auto_complete_text_for_location(location)
#
#
# def auto_complete_text(self):
#     return LocationAutoComplete.objects.get(location=self).text
#
#
# Location.auto_complete_text = auto_complete_text
예제 #5
0
class Task(TimeStampModel):

    SIZE_CHOICES = (
        ('s', _('Small')),  # 1 hrs
        ('m', _('Medium')),  # 2-4 hrs
        ('l', _('Large')),  # 5+ hrs
    )

    VEHICLE_CHOICES = (
        ('b', _('Bicycle')),
        ('m', _('Motorcycle')),
        ('c', _('Car')),
        ('v', _('Van')),
        ('t', _('Truck')),
    )

    STATUS_CHOICES = (
        ('a', _('Active')),
        ('h', _('Has Deal')),
        ('d', _('Done')),
    )

    tasker = models.ForeignKey(UserModel,
                               on_delete=models.CASCADE,
                               related_name='tasker')
    client = models.ForeignKey(UserModel,
                               on_delete=models.CASCADE,
                               related_name='client')
    address = models.ForeignKey(Address,
                                on_delete=models.CASCADE,
                                related_name='address')
    category = TreeForeignKey(Category,
                              on_delete=models.CASCADE,
                              related_name='category')
    title = models.CharField(max_length=250)
    description = models.TextField(blank=True, null=True)
    status = models.CharField(max_length=1,
                              choices=STATUS_CHOICES,
                              default='a')
    price = models.DecimalField(decimal_places=2, max_digits=12, default=0)
    size = models.CharField(max_length=1, choices=SIZE_CHOICES, default='s')
    vehicle_requirement = models.CharField(max_length=1,
                                           choices=VEHICLE_CHOICES,
                                           blank=True,
                                           null=True)
    start_time = models.DateTimeField(blank=True, null=True)
    duration = models.IntegerField(validators=[
        MinValueValidator(1, _("Enter value greater than or equal 1 day")),
        MaxValueValidator(30, _("Enter value less than or equal 30 days"))
    ])
    client_phone = PhoneNumberField(blank=True, null=True)

    def __str__(self):
        return "Task (%s)" % self.id

    @property
    def has_deal(self):
        return True if self.deals else False

    @property
    def has_deal_accepted(self):
        return True if TaskDeal.objects.filter(task=self,
                                               is_accepted=True) else False
예제 #6
0
class Category(MPTTModel):
    parent = TreeForeignKey('self',
                            verbose_name='Üst Kategori',
                            on_delete=models.CASCADE,
                            null=True,
                            blank=True,
                            related_name='children',
                            db_index=True)
    category_name = models.CharField('Kategori Ismi',
                                     max_length=30,
                                     null=True,
                                     blank=True)
    category_defination = models.CharField('Kategori Açıklaması',
                                           max_length=140,
                                           null=True,
                                           blank=True)
    category_logo = models.CharField('Kategori Logo',
                                     max_length=50,
                                     null=True,
                                     blank=True)
    category_slug = models.SlugField('Slug', null=True, blank=True)
    image_prod = VersatileImageField('Kategori Resmi',
                                     upload_to=upload_location,
                                     null=True,
                                     blank=True,
                                     width_field="width_field",
                                     height_field="height_field")
    height_field = models.PositiveIntegerField('Uzunluk Değeri',
                                               default=0,
                                               blank=True)
    width_field = models.PositiveIntegerField('Genişlik Değeri',
                                              default=0,
                                              blank=True)
    created_at = models.DateTimeField('Oluşturulma Tarihi',
                                      auto_now_add=True,
                                      editable=False)
    updated_at = models.DateTimeField('Güncellenme Tarihi',
                                      auto_now=True,
                                      editable=False)

    class Meta:
        unique_together = ((
            'parent',
            'category_slug',
        ))
        verbose_name = 'Kategori'
        verbose_name_plural = 'Kategoriler'
        ordering = ('-created_at', )

    class MPTTMeta:
        order_insertion_by = ['category_name']

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

    def root_node(self):
        the_parent = self.tree_id
        return the_parent

    def get_parent(self):
        the_parent = self
        if self.parent:
            the_parent = self.parent
        return the_parent

    def get_children(self):
        parent = self.get_parent()
        qs = Category.objects.filter(parent=parent)
        qs_parent = Category.objects.filter(pk=parent.pk)
        return (qs | qs_parent)

    def image_cat(self):
        if self.image_prod:
            return mark_safe(
                '<img src="%s" style="width: 100px; height:100px;" />' %
                self.image_prod.url)
        else:
            return 'Kategori Resmi Bulunamadı'

    image_cat.short_description = 'Kategori Resmi'

    def image_logo_cat(self):
        flat = 'fa flaticon-'
        if self.category_logo:
            return mark_safe('<i class="%s%s"></i>' %
                             (flat, self.category_logo))
        else:
            return 'Kategori Logosu Bulunamadı'

    image_logo_cat.short_description = 'Kategori Logosu'
예제 #7
0
class Forum(MPTTModel, TimeStampedModel):

    courseevent = models.ForeignKey(CourseEvent)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            db_index=True,
                            verbose_name="einhängen unter")

    title = models.CharField(verbose_name="Forums-Titel", max_length=100)
    text = models.TextField(
        verbose_name="Ausführliche Beschreibung des Forums",
        help_text=
        """Dieser Text wird den Forumsbeiträgen vorangestellt und leitet die Kursteilnehmern an, ihre
                  Beiträge zu schreiben.""",
        blank=True)
    description = models.CharField(
        verbose_name="Kurzbeschreibung",
        help_text=
        "Die Kurzbeschreibung erscheint auf der Übersichtsseite der Foren.",
        max_length=200,
        blank=True)
    display_nr = models.IntegerField(verbose_name="Anzeigereihenfolge",
                                     help_text="nicht nach aussen sichtbar")

    can_have_threads = models.BooleanField(
        verbose_name="Beiträge erlaubt",
        help_text=
        """Steuert, ob Beiträge in diesem Unterforum gemacht werden können,
                  oder ob es nur zur Gliederung dient.""",
        default=True)

    objects = ForumManager()
    forum = QueryManager(level=0)
    subforum = QueryManager(level__gte=1)
    parent_choice_new = QueryManager(level__lte=1).order_by(
        'tree_id', 'level', 'display_nr')

    class Meta:
        verbose_name = "Forum"
        verbose_name_plural = "Foren"

    class MPTTMeta:
        order_insertion_by = ['courseevent', 'display_nr']

    def __unicode__(self):
        return u'%s' % (self.title)

    def get_next_sibling(self):
        next = super(Forum, self).get_next_sibling()
        try:
            if next.courseevent_id == self.courseevent_id:
                return next
            else:
                return None
        except:
            return None

    def get_previous_sibling(self):
        previous = super(Forum, self).get_previous_sibling()
        try:
            if previous.courseevent_id == self.courseevent_id:
                return previous
            else:
                return None
        except:
            return None

    def get_breadcrumbs_with_self(self):
        return self.get_ancestors(include_self=True)

    def get_published_breadcrumbs_with_self(self):
        return self.get_ancestors(include_self=True).filter(level__gt=0)

    @property
    def thread_count(self):
        return Thread.objects.filter(forum=self).count()

    @property
    def decendants_thread_count(self):
        decendants_ids = self.get_descendants(include_self=True).values_list(
            'id', flat=True)
        return Thread.objects.filter(forum__in=decendants_ids).count()

    @property
    def is_visible_in_classroom(self):
        """
        decides whether forum is visible in classroom
        """
        # fetched here in order to avoid circular import
        if self.level == 1:
            level1_forum = self
        else:
            level1_forum = self.get_ancestors().get(level=1)
        # fetched here in order to avoid circular import
        from apps_data.courseevent.models.menu import ClassroomMenuItem
        menuitems = \
            ClassroomMenuItem.objects.forum_ids_published_in_class(courseevent=self.courseevent)
        if level1_forum.id in menuitems:
            return True
        else:
            return False

    def clean(self):
        if not self.can_have_threads:
            if self.thread_count > 0:
                raise ValidationError('Dieses Forum hat bereits Beiträge.')
예제 #8
0
class MetricCache(models.Model):
    project = TreeForeignKey(Project, related_name='cached_scores')
    metric = TreeForeignKey(Metric, related_name='cached_scores')
    raw_value = JSONField(null=True)
    score = models.FloatField()
    start = models.DateTimeField(null=True)
    end = models.DateTimeField(null=True)
    is_complete = models.BooleanField()
    is_dirty = models.BooleanField(default=False)

    objects = QuerySetManager()

    class Meta:
        unique_together = ('project', 'metric', 'start', 'end')

    def __init__(self, *args, **kwargs):
        # Allow children to be injected in
        try:
            self._children = kwargs.pop('children')
        except KeyError:
            pass

        super(MetricCache, self).__init__(*args, **kwargs)

    def update_id(self):
        """
        Set .id so that we update existing entries if a score tree already
        exists.
        """
        # .id is already set, so don't do anything
        if self.id is not None:
            return

        try:
            self.id = type(self).objects.get(project=self.project,
                                             metric=self.metric,
                                             start=self.start,
                                             end=self.end).id
        except type(self).DoesNotExist:
            pass

        except MultipleObjectsReturned:
            logger.warn(
                "Duplicate MetricCache scores found in the database "
                "matching (%s, %s, %s, %s). "
                "Picking the first one...", self.project, self.metric,
                self.start, self.end)

            self.id = type(self).objects.filter(project=self.project,
                                                metric=self.metric,
                                                start=self.start,
                                                end=self.end)[:1][0].id

    def save_all(self):
        """
        Recursively save results into database
        """
        try:
            children = self.children

        except AttributeError:
            children = []

        for child in children:
            child.save_all()

        self.update_id()
        self.save()

    # virtual fields
    @cached_property
    def parent(self):
        try:
            return self._parent
        except AttributeError:
            return self.metric.parent.get_result(project=self.project,
                                                 start=self.start,
                                                 end=self.end)

    @cached_property
    def children(self):
        try:
            return self._children
        except AttributeError:
            qs = type(self).objects \
                           .filter(project=self.project_id,
                                   start=self.start, end=self.end,
                                   metric__in=self.metric.get_children()) \
                           .select_related('metric__algorithm') \
                           .order_by('metric__lft')

            for metric in qs:
                metric._parent = self

            return qs

    @cached_property
    def colour(self):
        return score_to_colour(self.score)

    @cached_property
    def weight(self):
        return self.metric.weight

    @cached_property
    def weight_normalized_score(self):
        return (self.score / 100.0 * self.weight)

    @cached_property
    def normalized_weight(self):
        if self.metric.parent_id is None:
            return 100

        total = sum(m.weight for m in self.parent.children)

        return float(self.metric.weight) / total * 100

    @cached_property
    def leaf_children(self):
        return [child for child in self.children if not child.children]

    @cached_property
    def aggregate_children(self):
        return [child for child in self.children if child.children]

    class QuerySet(models.query.QuerySet):
        def columnwise(self, cols_before=[], cols_after=[]):
            args = cols_before[:]
            args.extend(('metric__tree_id', 'metric__lft', 'metric__id'))
            args.extend(cols_after)

            return self.order_by(*args)

        def iter_by_metric(self, metric_cols, cols, datalist_order=None):
            """
            Helper function to allow for iterating using a nested loop,
            grouped by metric.

            Yields an AttrDict keyed by metric_cols, with a "datalist" key
            containing AttrDict's keyed by cols.

            @param metric_cols List of columns from the Metric table to query
            @param cols List of columns from MetricCache table to query
            @param datalist_order 2-tuple of (key, ordered_value_list)
            """
            # Make sure we select the metric.id column
            metric_cols = set(metric_cols) | set(['id'])

            cols = ['metric__' + i for i in metric_cols] + cols

            metricid = None
            current_metric = None
            current_datalist = {}

            if datalist_order is not None:
                datalist_order_lookup = dict(
                    zip(datalist_order[1], itertools.count()))

                def sort_keyfn(data):
                    return datalist_order_lookup[data[datalist_order[0]]]

                def sort_datalist():
                    current_metric.datalist = sorted(current_metric.datalist,
                                                     key=sort_keyfn)
            else:

                def sort_datalist():
                    pass

            for row in self.columnwise(cols_after=cols).values(*cols):
                # new metric
                if row['metric__id'] != metricid:
                    if current_metric is not None:
                        sort_datalist()
                        yield current_metric
                    metricid = row['metric__id']
                    current_metric = AttrDict(
                        (k, row['metric__' + k]) for k in metric_cols)
                    current_metric.datalist = []

                # decode raw_value if not null
                current_data = AttrDict((k, row[k]) for k in cols)
                value = current_data.get('raw_value', None)
                if value is not None:
                    current_data['raw_value'] = json.loads(value)

                current_metric.datalist.append(current_data)

            if current_metric is not None:
                sort_datalist()
                yield current_metric

        def get_latest(self):
            return self.filter(end=None).columnwise()
예제 #9
0
class TaskArea(MPTTModel, CreatedModifiedModel):
    parent = TreeForeignKey('self', blank=True, null=True, related_name='children')
    name = models.CharField(max_length=255)

    def __unicode__(self):
        return self.name
예제 #10
0
class OrgUnit(MPTTModel):
    """Represents an element within the Department organisational hierarchy.
    """
    TYPE_CHOICES = (
        (0, 'Department (Tier one)'),
        (1, 'Division (Tier two)'),
        (11, 'Division'),
        (9, 'Group'),
        (2, 'Branch'),
        (7, 'Section'),
        (3, 'Region'),
        (6, 'District'),
        (8, 'Unit'),
        (5, 'Office'),
        (10, 'Work centre'),
    )
    TYPE_CHOICES_DICT = dict(TYPE_CHOICES)
    unit_type = models.PositiveSmallIntegerField(choices=TYPE_CHOICES)
    ad_guid = models.CharField(
        max_length=48, unique=True, null=True, editable=False)
    ad_dn = models.CharField(
        max_length=512, unique=True, null=True, editable=False)
    name = models.CharField(max_length=256, unique=True)
    acronym = models.CharField(max_length=16, null=True, blank=True)
    manager = models.ForeignKey(
        DepartmentUser, on_delete=models.PROTECT, null=True, blank=True)
    parent = TreeForeignKey(
        'self', on_delete=models.PROTECT, null=True, blank=True,
        related_name='children', db_index=True)
    details = JSONField(null=True, blank=True)
    location = models.ForeignKey(
        Location, on_delete=models.PROTECT, null=True, blank=True)
    secondary_location = models.ForeignKey(
        SecondaryLocation, on_delete=models.PROTECT, null=True, blank=True)
    sync_o365 = models.BooleanField(
        default=True, help_text='Sync this to O365 (creates a security group).')
    active = models.BooleanField(default=True)

    class MPTTMeta:
        order_insertion_by = ['name']

    class Meta:
        ordering = ('name',)

    def cc(self):
        try:
            return self.costcentre
        except:
            return None

    def __str__(self):
        name = self.name
        if self.acronym:
            name = '{} - {}'.format(self.acronym, name)
        if self.cc():
            return '{} - CC {}'.format(name, self.cc())
        return name

    def members(self):
        return DepartmentUser.objects.filter(org_unit__in=self.get_descendants(
            include_self=True), **DepartmentUser.ACTIVE_FILTER)

    def save(self, *args, **kwargs):
        self.details = self.details or {}
        self.details.update({
            'type': self.get_unit_type_display(),
        })
        super(OrgUnit, self).save(*args, **kwargs)
        if not getattr(self, 'cheap_save', False):
            for user in self.members():
                user.save()

    def get_descendants_active(self, *args, **kwargs):
        """Exclude 'inactive' OrgUnit objects from get_descendants() queryset.
        Returns a list of OrgUnits.
        """
        descendants = self.get_descendants(*args, **kwargs).exclude(active=False)
        return descendants
예제 #11
0
파일: models.py 프로젝트: AYCHSearch/RDMO
class Attribute(MPTTModel):

    uri = models.URLField(
        max_length=640,
        blank=True,
        verbose_name=_('URI'),
        help_text=
        _('The Uniform Resource Identifier of this attribute (auto-generated).'
          ))
    uri_prefix = models.URLField(
        max_length=256,
        verbose_name=_('URI Prefix'),
        help_text=_('The prefix for the URI of this attribute.'))
    key = models.SlugField(
        max_length=128,
        blank=True,
        verbose_name=_('Key'),
        help_text=_('The internal identifier of this attribute.'))
    path = models.CharField(
        max_length=512,
        db_index=True,
        verbose_name=_('Path'),
        help_text=_(
            'The path part of the URI of this attribute (auto-generated).'))
    comment = models.TextField(
        blank=True,
        verbose_name=_('Comment'),
        help_text=_('Additional information about this attribute.'))
    parent = TreeForeignKey(
        'self',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='children',
        db_index=True,
        verbose_name=_('Parent attribute'),
        help_text=_('Parent attribute in the domain model.'))

    class Meta:
        ordering = ('uri', )
        verbose_name = _('Attribute')
        verbose_name_plural = _('Attributes')

    def __str__(self):
        return self.uri or self.key

    def save(self, *args, **kwargs):
        self.path = Attribute.build_path(self.key, self.parent)
        self.uri = get_uri_prefix(self) + '/domain/' + self.path

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

        # recursively save children
        for child in self.children.all():
            child.save()

    def clean(self):
        self.path = Attribute.build_path(self.key, self.parent)
        AttributeUniquePathValidator(self)()

    @classmethod
    def build_path(self, key, parent):
        path = key
        while parent:
            path = parent.key + '/' + path
            parent = parent.parent
        return path
예제 #12
0
class DepartmentUser(MPTTModel):
    """Represents a Department user. Maps to an object managed by Active Directory.
    """
    ACTIVE_FILTER = {"active": True, "email__isnull": False,
                     "cost_centre__isnull": False, "contractor": False}
    # The following choices are intended to match options in Alesco.
    ACCOUNT_TYPE_CHOICES = (
        (2, 'L1 User Account - Permanent'),
        (3, 'L1 User Account - Agency contract'),
        (0, 'L1 User Account - Department fixed-term contract'),
        (8, 'L1 User Account - Seasonal'),
        (6, 'L1 User Account - Vendor'),
        (7, 'L1 User Account - Volunteer'),
        (1, 'L1 User Account - Other/Alumni'),
        (11, 'L1 User Account - RoomMailbox'),
        (12, 'L1 User Account - EquipmentMailbox'),
        (10, 'L2 Service Account - System'),
        (5, 'L1 Group (shared) Mailbox - Shared account'),
        (9, 'L1 Role Account - Role-based account'),
        #(4, 'Resigned'),
        (4, 'Terminated'),
        (14, 'Unknown - AD disabled'),
        (15, 'Cleanup - Permanent'),
        (16, 'Unknown - AD active'),
    )
    POSITION_TYPE_CHOICES = (
        (0, 'Full time'),
        (1, 'Part time'),
        (2, 'Casual'),
        (3, 'Other'),
    )

    date_created = models.DateTimeField(auto_now_add=True)
    date_updated = models.DateTimeField(auto_now=True)
    cost_centre = models.ForeignKey(
        "organisation.CostCentre", on_delete=models.PROTECT, null=True)
    cost_centres_secondary = models.ManyToManyField(
        "organisation.CostCentre", related_name="cost_centres_secondary", editable=False,
        blank=True, help_text='NOTE: this provides security group access (e.g. T drives).')
    org_unit = models.ForeignKey(
        "organisation.OrgUnit", on_delete=models.PROTECT, null=True, blank=True,
        verbose_name='organisational unit',
        help_text="""The organisational unit that represents the user's"""
        """ primary physical location (also set their distribution group).""")
    org_units_secondary = models.ManyToManyField(
        "organisation.OrgUnit", related_name="org_units_secondary", blank=True, editable=False,
        help_text='NOTE: this provides email distribution group access.')
    extra_data = JSONField(null=True, blank=True)
    ad_guid = models.CharField(
        max_length=48, unique=True, null=True, blank=True, verbose_name='AD GUID',
        help_text='Locally stored AD GUID. This field must match GUID in the AD object for sync to be successful')
    azure_guid = models.CharField(
        max_length=48, unique=True, null=True, blank=True, verbose_name='Azure GUID',
        help_text='Azure AD GUID.')
    ad_dn = models.CharField(
        max_length=512, unique=True, null=True, blank=True, verbose_name='AD DN',
        help_text='AD DistinguishedName value.')
    ad_data = JSONField(null=True, blank=True, editable=False)
    org_data = JSONField(null=True, blank=True, editable=False)
    employee_id = models.CharField(
        max_length=128, null=True, unique=True, blank=True, verbose_name='Employee ID',
        help_text='HR Employee ID.')
    email = models.EmailField(unique=True)
    username = models.CharField(
        max_length=128, editable=False, blank=True, null=True,
        help_text='Pre-Windows 2000 login username.')
    name = models.CharField(max_length=128, help_text='Format: [Given name] [Surname]')
    given_name = models.CharField(
        max_length=128, null=True,
        help_text='Legal first name (matches birth certificate/password/etc.)')
    surname = models.CharField(
        max_length=128, null=True,
        help_text='Legal surname (matches birth certificate/password/etc.)')
    name_update_reference = models.CharField(
        max_length=512, null=True, blank=True, verbose_name='update reference',
        help_text='Reference for name/CC change request')
    preferred_name = models.CharField(
        max_length=256, null=True, blank=True,
        help_text='Employee-editable preferred name.')
    title = models.CharField(
        max_length=128, null=True,
        help_text='Occupation position title (should match Alesco)')
    position_type = models.PositiveSmallIntegerField(
        choices=POSITION_TYPE_CHOICES, null=True, blank=True, default=0,
        help_text='Employee position working arrangement (should match Alesco status)')
    parent = TreeForeignKey(
        'self', on_delete=models.PROTECT, null=True, blank=True,
        related_name='children', editable=True, verbose_name='Reports to',
        help_text='Person that this employee reports to')
    expiry_date = models.DateTimeField(
        null=True, blank=True, help_text='Date that the AD account is set to expire.')
    date_ad_updated = models.DateTimeField(
        null=True, editable=False, verbose_name='Date AD updated',
        help_text='The date when the AD account was last updated.')
    telephone = models.CharField(max_length=128, null=True, blank=True)
    mobile_phone = models.CharField(max_length=128, null=True, blank=True)
    extension = models.CharField(
        max_length=128, null=True, blank=True, verbose_name='VoIP extension')
    home_phone = models.CharField(max_length=128, null=True, blank=True)
    other_phone = models.CharField(max_length=128, null=True, blank=True)
    active = models.BooleanField(
        default=True, editable=False,
        help_text='Account is active within Active Directory.')
    ad_deleted = models.BooleanField(
        default=False, editable=False, verbose_name='AD deleted',
        help_text='Account has been deleted in Active Directory.')
    in_sync = models.BooleanField(
        default=False, editable=False,
        help_text='CMS data has been synchronised from AD data.')
    vip = models.BooleanField(
        default=False,
        help_text="An individual who carries out a critical role for the department")
    executive = models.BooleanField(
        default=False, help_text="An individual who is an executive")
    contractor = models.BooleanField(
        default=False,
        help_text="An individual who is an external contractor (does not include agency contract staff)")
    photo = models.ImageField(blank=True, upload_to=get_photo_path)
    photo_ad = models.ImageField(
        blank=True, editable=False, upload_to=get_photo_ad_path)
    sso_roles = models.TextField(
        null=True, editable=False, help_text="Groups/roles separated by semicolon")
    notes = models.TextField(
        null=True, blank=True,
        help_text='Records relevant to any AD account extension, expiry or deletion (e.g. ticket #).')
    working_hours = models.TextField(
        default="N/A", null=True, blank=True,
        help_text="Description of normal working hours")
    secondary_locations = models.ManyToManyField("organisation.Location", blank=True,
        help_text="Only to be used for staff working in additional loactions from their cost centre")
    populate_primary_group = models.BooleanField(
        default=True,
        help_text="If unchecked, user will not be added to primary group email")
    account_type = models.PositiveSmallIntegerField(
        choices=ACCOUNT_TYPE_CHOICES, null=True, blank=True,
        help_text='Employee account status (should match Alesco status)')
    alesco_data = JSONField(
        null=True, blank=True, help_text='Readonly data from Alesco')
    security_clearance = models.BooleanField(
        default=False, verbose_name='security clearance granted',
        help_text='''Security clearance approved by CC Manager (confidentiality
        agreement, referee check, police clearance, etc.''')
    o365_licence = models.NullBooleanField(
        default=None, editable=False,
        help_text='Account consumes an Office 365 licence.')
    shared_account = models.BooleanField(
        default=False, editable=False,
        help_text='Automatically set from account type.')

    class MPTTMeta:
        order_insertion_by = ['name']

    class Meta:
        ordering = ('name',)

    def __init__(self, *args, **kwargs):
        super(DepartmentUser, self).__init__(*args, **kwargs)
        # Store the pre-save values of some fields on object init.
        self.__original_given_name = self.given_name
        self.__original_surname = self.surname
        self.__original_employee_id = self.employee_id
        self.__original_cost_centre = self.cost_centre
        self.__original_name = self.name
        self.__original_org_unit = self.org_unit
        self.__original_expiry_date = self.expiry_date

    def __str__(self):
        return self.email

    def save(self, *args, **kwargs):
        """Override the save method with additional business logic.
        """
        if self.employee_id:
            if (self.employee_id.lower() == "n/a") or (self.employee_id.strip() == ''):
                self.employee_id = None
        self.in_sync = True if self.date_ad_updated else False
        # If the CC is set but not the OrgUnit, use the CC's OrgUnit.
        if self.cost_centre and not self.org_unit:
            self.org_unit = self.cost_centre.org_position
        if self.cost_centre and self.org_unit:
            self.org_data = self.org_data or {}
            self.org_data["units"] = list(self.org_unit.get_ancestors(include_self=True).values(
                "id", "name", "acronym", "unit_type", "costcentre__code",
                "costcentre__name", "location__name"))
            self.org_data["unit"] = self.org_data["units"][-1]
            if self.org_unit.location:
                self.org_data["location"] = self.org_unit.location.as_dict()
            if self.org_unit.secondary_location:
                self.org_data[
                    "secondary_location"] = self.org_unit.secondary_location.as_dict()
            for unit in self.org_data["units"]:
                unit["unit_type"] = self.org_unit.TYPE_CHOICES_DICT[
                    unit["unit_type"]]
            self.org_data["cost_centre"] = {
                "name": self.org_unit.name,
                "code": self.cost_centre.code,
                "cost_centre_manager": str(self.cost_centre.manager),
                "business_manager": str(self.cost_centre.business_manager),
                "admin": str(self.cost_centre.admin),
                "tech_contact": str(self.cost_centre.tech_contact),
            }
            if self.cost_centres_secondary.exists():
                self.org_data['cost_centres_secondary'] = [{
                    'name': i.name,
                    'code': i.code,
                } for i in self.cost_centres_secondary.all()]
            if self.org_units_secondary:
                self.org_data['org_units_secondary'] = [{
                    'name': i.name,
                    'acronym': i.name,
                    'unit_type': i.get_unit_type_display(),
                } for i in self.org_units_secondary.all()]
        try:
            self.update_photo_ad()
        except:  # Don't bomb out of saving for update_photo_ad errors.
            pass
        if self.account_type in [5, 9]:  # Shared/role-based account types.
            self.shared_account = True
        super(DepartmentUser, self).save(*args, **kwargs)

    def update_photo_ad(self):
        # Update self.photo_ad to a 240x240 thumbnail >10 kb in size.
        if not self.photo:
            if self.photo_ad:
                self.photo_ad.delete()
            return

        from PIL import Image
        from six import BytesIO
        from django.core.files.base import ContentFile

        if hasattr(self.photo.file, 'content_type'):
            PHOTO_TYPE = self.photo.file.content_type

            if PHOTO_TYPE == 'image/jpeg':
                PIL_TYPE = 'jpeg'
            elif PHOTO_TYPE == 'image/png':
                PIL_TYPE = 'png'
            else:
                return
        else:
            PIL_TYPE = 'jpeg'
        # good defaults to get ~10kb JPEG images
        PHOTO_AD_SIZE = (240, 240)
        PIL_QUALITY = 75
        # remote file size limit
        PHOTO_AD_FILESIZE = 10000

        image = Image.open(BytesIO(self.photo.read()))
        image.thumbnail(PHOTO_AD_SIZE, Image.LANCZOS)

        # in case we miss 10kb, drop the quality and recompress
        for i in range(12):
            temp_buffer = BytesIO()
            image.save(temp_buffer, PIL_TYPE,
                       quality=PIL_QUALITY, optimize=True)
            length = temp_buffer.tell()
            if length <= PHOTO_AD_FILESIZE:
                break
            if PIL_TYPE == 'png':
                PIL_TYPE = 'jpeg'
            else:
                PIL_QUALITY -= 5

        temp_buffer.seek(0)
        self.photo_ad.save(os.path.basename(self.photo.name),
                           ContentFile(temp_buffer.read()), save=False)

    def org_data_pretty(self):
        if not self.org_data:
            return self.org_data
        return format_html(json2html.convert(json=self.org_data))

    def ad_data_pretty(self):
        if not self.ad_data:
            return self.ad_data
        return format_html(json2html.convert(json=self.ad_data))

    def alesco_data_pretty(self):
        if not self.alesco_data:
            return self.alesco_data
        # Manually generate HTML table output, to guarantee field order.
        t = '''<table border="1">
            <tr><th>FIRST_NAME</th><td>{FIRST_NAME}</td></tr>
            <tr><th>SECOND_NAME</th><td>{SECOND_NAME}</td></tr>
            <tr><th>SURNAME</th><td>{SURNAME}</td></tr>
            <tr><th>EMPLOYEE_NO</th><td>{EMPLOYEE_NO}</td></tr>
            <tr><th>PAYPOINT</th><td>{PAYPOINT}</td></tr>
            <tr><th>PAYPOINT_DESC</th><td>{PAYPOINT_DESC}</td></tr>
            <tr><th>MANAGER_POS#</th><td>{MANAGER_POS#}</td></tr>
            <tr><th>MANAGER_NAME</th><td>{MANAGER_NAME}</td></tr>
            <tr><th>JOB_NO</th><td>{JOB_NO}</td></tr>
            <tr><th>FIRST_COMMENCE</th><td>{FIRST_COMMENCE}</td></tr>
            <tr><th>OCCUP_TERM_DATE</th><td>{OCCUP_TERM_DATE}</td></tr>
            <tr><th>POSITION_NO</th><td>{POSITION_NO}</td></tr>
            <tr><th>OCCUP_POS_TITLE</th><td>{OCCUP_POS_TITLE}</td></tr>
            <tr><th>LOC_DESC</th><td>{LOC_DESC}</td></tr>
            <tr><th>CLEVEL1_ID</th><td>{CLEVEL1_ID}</td></tr>
            <tr><th>CLEVEL2_DESC</th><td>{CLEVEL2_DESC}</td></tr>
            <tr><th>CLEVEL3_DESC</th><td>{CLEVEL3_DESC}</td></tr>
            <tr><th>EMP_STAT_DESC</th><td>{EMP_STAT_DESC}</td></tr>
            <tr><th>GEO_LOCATION_DESC</th><td>{GEO_LOCATION_DESC}</td></tr>
            </table>'''
        t = t.format(**self.alesco_data)
        return mark_safe(t)

    @property
    def password_age_days(self):
        if self.ad_data and 'pwdLastSet' in self.ad_data:
            try:
                td = datetime.now() - convert_ad_timestamp(self.ad_data['pwdLastSet'])
                return td.days
            except:
                pass
        return None

    @property
    def ad_expired(self):
        if self.expiry_date and self.expiry_date < timezone.now():
            return True
        return False

    def get_gal_department(self):
        """Return a string to place into the "Department" field for the Global Address List.
        """
        s = ''
        if self.org_data and 'units' in self.org_data:
            s = self.org_data['units'][0]['acronym']
            if len(self.org_data['units']) > 1:
                s += ' - {}'.format(self.org_data['units'][1]['name'])
        return s
예제 #13
0
class FilterNode(MPTTModel):
    parent = TreeForeignKey('self',
                            related_name='children',
                            blank=True,
                            null=True)
    name = models.CharField(max_length=200, null=True, blank=True)
    prop_name = models.CharField(max_length=200, null=True, blank=True)
    operator = EnumField(FilterOperator, max_length=200, null=True, blank=True)
    value = models.CharField(max_length=200, null=True, blank=True)
    annotations = models.ManyToManyField('Annotation',
                                         related_name='attachment',
                                         blank=True)

    content_type = models.ForeignKey(ContentType)

    objects = FilterManager()

    def __unicode__(self):
        if self.name:
            return self.name + ": " + self.as_string()
        return self.as_string()

    @property
    def results(self):
        results = cache.get('filters:%s' % (self.pk))
        cls = self.content_type.model_class()
        results = None
        if results is None:
            results = list(
                self.apply(cls.objects.all()).values_list('pk', flat=True))
            cache.set('filters:%s' % (self.pk), results, 300)
        return self.apply_annotations(cls.objects.filter(pk__in=results))

    def save(self, *args, **kwargs):
        if self.pk is not None:
            cache.delete('filters:%s' % (self.pk))
        return super(FilterNode, self).save(*args, **kwargs)

    def as_string(self):
        op = self.operator
        if op == FilterOperator.AND or op == FilterOperator.OR:
            joiner = ", %s " % (op)
            return "(%s)" % (joiner.join(c.as_string()
                                         for c in self.children.all()))
        elif op == FilterOperator.NOT:
            if self.children.exists():
                return "not %s" % (self.children.first().as_string())
            return "not (???)"
        elif op == FilterOperator.LESS_THAN_EQUAL:
            op = "<="
        elif op == FilterOperator.GREATER_THAN_EQUAL:
            op = ">="
        elif op == FilterOperator.GREATER_THAN:
            op = ">"
        elif op == FilterOperator.LESS_THAN:
            op = "<"
        return "%s %s %s" % (self.prop_name, op, self.value)

    def apply_annotations(self, queryset):
        annotations = {}
        for child in self.get_family():
            for annotation in child.annotations.all():
                annotations.update(annotation.as_dict())
        return queryset.annotate(**annotations)

    def apply(self, queryset):
        return self.apply_annotations(queryset).filter(self.as_filter())

    def as_filter(self):
        if self.operator == FilterOperator.AND:
            ret = Q()
            for c in self.get_children().all():
                ret = ret & c.as_filter()
            return ret
        if self.operator == FilterOperator.OR:
            ret = Q()
            for c in self.get_children().all():
                ret = ret | c.as_filter()
            return ret
        if self.operator == FilterOperator.NOT:
            return ~self.get_children().first().as_filter()
        if self.operator == FilterOperator.IS_TRUE:
            return Q(**{self.prop_name: True})
        if self.operator == FilterOperator.IS_FALSE:
            return Q(**{self.prop_name: True})
        if self.operator == FilterOperator.IS_EMPTY:
            return Q(**{self.prop_name + '__isnull': True})
        if self.operator == FilterOperator.IS_NOT_EMPTY:
            return Q(**{self.prop_name + '__isnull': False})
        value = self.value
        if value is None:
            value = ""
        if value.startswith("+") or value.startswith("-"):
            direction = value[0]
            unit = value[-1]
            quantity = int(direction + value[1:-1])
            diff = timedelta()

            if unit == "y":
                diff = timedelta(days=quantity * 365)
            elif unit == "d":
                diff = timedelta(days=quantity)
            elif unit == "h":
                diff = timedelta(hours=quantity)
            elif unit == "m":
                diff = timedelta(minutes=quantity)

            value = timezone.now() + diff
        if self.operator == FilterOperator.IS:
            params = {self.prop_name: value}
        else:
            params = {"%s__%s" % (self.prop_name, self.operator.value): value}
        return Q(**params)
예제 #14
0
class StorageCategory(MPTTModel):
    parent = TreeForeignKey('StorageCategory', on_delete=models.DO_NOTHING, null=True, default=None, blank=True)
    name = models.TextField()
    description = models.TextField(blank=True, default='')
    def __unicode__(self):
        return '%d: %s' % (self.id, self.name)
예제 #15
0
class Account(MPTTModel):
    """ Represents an account

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

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

    Attributes:

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


    """

    TYPES = Choices(
        ("AS", "asset", "Asset"),  # Eg. Cash in bank
        ("LI", "liability",
         "Liability"),  # Eg. Loans, bills paid after the fact (in arrears)
        ("IN", "income", "Income"),  # Eg. Sales, housemate contributions
        ("EX", "expense", "Expense"),  # Eg. Office supplies, paying bills
        ("EQ", "equity", "Equity"),  # Eg. Money from shares
        ("TR", "trading",
         "Currency Trading"),  # Used to represent currency conversions
    )
    uuid = SmallUUIDField(default=uuid_default(), editable=False)
    name = models.CharField(max_length=50)
    parent = TreeForeignKey(
        "self",
        null=True,
        blank=True,
        related_name="children",
        db_index=True,
        on_delete=models.CASCADE,
    )
    code = models.CharField(max_length=3, null=True, blank=True)
    full_code = models.CharField(max_length=100,
                                 db_index=True,
                                 unique=True,
                                 null=True,
                                 blank=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():
            try:
                balance = self.balance()
            except ValueError:
                if self.full_code:
                    return "{} {}".format(self.full_code, name)
                else:
                    return name
            else:
                if self.full_code:
                    return "{} {} [{}]".format(self.full_code, name, balance)
                else:
                    return "{} [{}]".format(name, balance)

        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, leg_query=None, **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,
                                   leg_query=leg_query,
                                   **kwargs)
            for account in self.get_descendants(include_self=True)
        ]
        return sum(balances, Balance())

    def simple_balance(self, as_of=None, raw=False, leg_query=None, **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.
            leg_query (models.Q): Django Q-expression, will be used to filter the transaction legs.
                                  allows for more complex filtering than that provided by **kwargs.
            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 leg_query or kwargs:
            leg_query = leg_query or models.Q()
            legs = legs.filter(leg_query, **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
        elif self.type == self.TYPES.liability and to_account.type == self.TYPES.expense:
            # Transfers from liability -> asset accounts should reduce both.
            # For example, moving money from Rent Payable (liability) to your Rent (expense) account
            # should use the funds you've built up in the liability account to pay off the expense account.
            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
예제 #16
0
class Category(MPTTModel):
    parent = TreeForeignKey('self',
                            null=True,
                            on_delete=models.CASCADE,
                            blank=True,
                            related_name='children')
    name = models.CharField(max_length=50)
    slug = AutoSlugField(max_length=50, overwrite=True, populate_from='name')
    path = models.CharField(max_length=255)
    value = models.CharField(max_length=255)
    refinement = models.ForeignKey('Refinement', on_delete=models.CASCADE)

    class MPTTMeta:
        verbose_name_plural = "categories"
        unique_together = (("name", "slug", "parent"), )
        ordering = ("tree_id", "lft")

    @staticmethod
    def get_all_filtered_by_refinement_id(refinement_id):
        """ Get all categories filtered by refinement id.

        Parameters:
                refinement_id:

        Returns: Category collection

        """
        return Category.objects.all().filter(refinement=refinement_id)

    @staticmethod
    def create_and_save(name, path, value, parent, refinement):
        """ Create and save a category.

        Args:
            name:
            path:
            value:
            parent:
            refinement:

        Returns:

        """
        return Category.objects.create(name=name,
                                       path=path,
                                       value=value,
                                       parent=parent,
                                       refinement=refinement)

    @staticmethod
    def get_by_id(category_id):
        """ Get category by its id.

        Parameters:
            category_id:

        Returns:
            Category object

        """
        try:
            return Category.objects.get(pk=category_id)
        except Category.DoesNotExist as e:
            raise exceptions.DoesNotExist(e.message)
        except Exception as ex:
            raise exceptions.ModelError(ex.message)

    @staticmethod
    def get_all_categories_by_parent_slug_and_refinement_id(
            parent_slug, refinement_id):
        """ Get all categories by parent_slug and refinement.

        Args:
            parent_slug:
            refinement_id:

        Returns:

        """
        try:
            return Category.objects.get(
                slug__startswith=parent_slug,
                refinement_id=refinement_id).get_family()
        except Category.DoesNotExist as e:
            raise exceptions.DoesNotExist(e.message)
        except Exception as ex:
            raise exceptions.ModelError(ex.message)

    @staticmethod
    def get_all():
        """ Get all categories.

        Returns: Category collection

        """
        return Category.objects.all()
예제 #17
0
class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel):
    """
    An Agenda Item

    MPTT-model. See http://django-mptt.github.com/django-mptt/
    """
    slide_callback_name = 'agenda'

    AGENDA_ITEM = 1
    ORGANIZATIONAL_ITEM = 2

    ITEM_TYPE = ((AGENDA_ITEM, ugettext_lazy('Agenda item')),
                 (ORGANIZATIONAL_ITEM, ugettext_lazy('Organizational item')))

    title = models.CharField(null=True,
                             max_length=255,
                             verbose_name=ugettext_lazy("Title"))
    """
    Title of the agenda item.
    """

    text = models.TextField(null=True,
                            blank=True,
                            verbose_name=ugettext_lazy("Text"))
    """
    The optional text of the agenda item.
    """

    comment = models.TextField(null=True,
                               blank=True,
                               verbose_name=ugettext_lazy("Comment"))
    """
    Optional comment to the agenda item. Will not be shoun to normal users.
    """

    closed = models.BooleanField(default=False,
                                 verbose_name=ugettext_lazy("Closed"))
    """
    Flag, if the item is finished.
    """

    type = models.IntegerField(max_length=1,
                               choices=ITEM_TYPE,
                               default=AGENDA_ITEM,
                               verbose_name=ugettext_lazy("Type"))
    """
    Type of the agenda item.

    See Item.ITEM_TYPE for more information.
    """

    duration = models.CharField(null=True, blank=True, max_length=5)
    """
    The intended duration for the topic.
    """

    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children')
    """
    The parent item in the agenda tree.
    """

    weight = models.IntegerField(default=0,
                                 verbose_name=ugettext_lazy("Weight"))
    """
    Weight to sort the item in the agenda.
    """

    content_type = models.ForeignKey(ContentType, null=True, blank=True)
    """
    Field for generic relation to a related object. Type of the object.
    """

    object_id = models.PositiveIntegerField(null=True, blank=True)
    """
    Field for generic relation to a related object. Id of the object.
    """
    # TODO: rename it to object_pk

    content_object = generic.GenericForeignKey()
    """
    Field for generic relation to a related object. General field to the related object.
    """

    speaker_list_closed = models.BooleanField(
        default=False,
        verbose_name=ugettext_lazy("List of speakers is closed"))
    """
    True, if the list of speakers is closed.
    """
    class Meta:
        permissions = (('can_see_agenda', ugettext_noop("Can see agenda")), (
            'can_manage_agenda', ugettext_noop("Can manage agenda")
        ), ('can_see_orga_items',
            ugettext_noop("Can see orga items and time scheduling of agenda")))

    class MPTTMeta:
        order_insertion_by = ['weight']

    def save(self, *args, **kwargs):
        super(Item, self).save(*args, **kwargs)
        if self.parent and self.parent.is_active_slide():
            update_projector()

    def __unicode__(self):
        return self.get_title()

    def get_absolute_url(self, link='detail'):
        """
        Return the URL to this item.

        The link can be detail, update or delete.
        """
        if link == 'detail':
            url = reverse('item_view', args=[str(self.id)])
        elif link == 'update':
            url = reverse('item_edit', args=[str(self.id)])
        elif link == 'delete':
            url = reverse('item_delete', args=[str(self.id)])
        elif link == 'projector_list_of_speakers':
            url = '%s&type=list_of_speakers' % super(
                Item, self).get_absolute_url('projector')
        elif link == 'projector_summary':
            url = '%s&type=summary' % super(Item,
                                            self).get_absolute_url('projector')
        elif (link in ('projector', 'projector_preview')
              and self.content_object
              and isinstance(self.content_object, SlideMixin)):
            url = self.content_object.get_absolute_url(link)
        else:
            url = super(Item, self).get_absolute_url(link)
        return url

    def get_title(self):
        """
        Return the title of this item.
        """
        if not self.content_object:
            return self.title
        try:
            return self.content_object.get_agenda_title()
        except AttributeError:
            raise NotImplementedError(
                'You have to provide a get_agenda_title method on your related model.'
            )

    def get_title_supplement(self):
        """
        Return a supplement for the title.
        """
        if not self.content_object:
            return ''
        try:
            return self.content_object.get_agenda_title_supplement()
        except AttributeError:
            raise NotImplementedError(
                'You have to provide a get_agenda_title_supplement method on your related model.'
            )

    def set_closed(self, closed=True):
        """
        Changes the closed-status of the item.
        """
        self.closed = closed
        self.save()

    @property
    def weight_form(self):
        """
        Return the WeightForm for this item.
        """
        from openslides.agenda.forms import ItemOrderForm
        try:
            parent = self.parent.id
        except AttributeError:
            parent = 0
        initial = {
            'weight': self.weight,
            'self': self.id,
            'parent': parent,
        }
        return ItemOrderForm(initial=initial, prefix="i%d" % self.id)

    def delete(self, with_children=False):
        """
        Delete the Item.
        """
        if not with_children:
            for child in self.get_children():
                child.move_to(self.parent)
                child.save()
        super(Item, self).delete()
        Item.objects.rebuild()

    def get_list_of_speakers(self,
                             old_speakers_count=None,
                             coming_speakers_count=None):
        """
        Returns the list of speakers as a list of dictionaries. Each
        dictionary contains a prefix, the speaker and its type. Types
        are old_speaker, actual_speaker and coming_speaker.
        """
        speaker_query = Speaker.objects.filter(
            item=self)  # TODO: Why not self.speaker_set?
        list_of_speakers = []

        # Parse old speakers
        old_speakers = speaker_query.exclude(begin_time=None).exclude(
            end_time=None).order_by('end_time')
        if old_speakers_count is None:
            old_speakers_count = old_speakers.count()
        last_old_speakers_count = max(
            0,
            old_speakers.count() - old_speakers_count)
        old_speakers = old_speakers[last_old_speakers_count:]
        for number, speaker in enumerate(old_speakers):
            prefix = old_speakers_count - number
            speaker_dict = {
                'prefix': '-%d' % prefix,
                'speaker': speaker,
                'type': 'old_speaker',
                'first_in_group': False,
                'last_in_group': False
            }
            if number == 0:
                speaker_dict['first_in_group'] = True
            if number == old_speakers_count - 1:
                speaker_dict['last_in_group'] = True
            list_of_speakers.append(speaker_dict)

        # Parse actual speaker
        try:
            actual_speaker = speaker_query.filter(end_time=None).exclude(
                begin_time=None).get()
        except Speaker.DoesNotExist:
            pass
        else:
            list_of_speakers.append({
                'prefix': '0',
                'speaker': actual_speaker,
                'type': 'actual_speaker',
                'first_in_group': True,
                'last_in_group': True
            })

        # Parse coming speakers
        coming_speakers = speaker_query.filter(
            begin_time=None).order_by('weight')
        if coming_speakers_count is None:
            coming_speakers_count = coming_speakers.count()
        coming_speakers = coming_speakers[:max(0, coming_speakers_count)]
        for number, speaker in enumerate(coming_speakers):
            speaker_dict = {
                'prefix': number + 1,
                'speaker': speaker,
                'type': 'coming_speaker',
                'first_in_group': False,
                'last_in_group': False
            }
            if number == 0:
                speaker_dict['first_in_group'] = True
            if number == coming_speakers_count - 1:
                speaker_dict['last_in_group'] = True
            list_of_speakers.append(speaker_dict)

        return list_of_speakers

    def get_next_speaker(self):
        """
        Returns the speaker object of the person who is next.
        """
        try:
            return self.speaker_set.filter(
                begin_time=None).order_by('weight')[0]
        except IndexError:
            # The list of speakers is empty.
            return None

    def is_active_slide(self):
        """
        Returns True if the slide is active. If the slide is a related item,
        Returns True if the related object is active.
        """
        if super(Item, self).is_active_slide():
            value = True
        elif self.content_object and isinstance(self.content_object,
                                                SlideMixin):
            value = self.content_object.is_active_slide()
        else:
            value = False
        return value
예제 #18
0
class Comment(MPTTModel):
    """
    Comment Model
    """
    message = models.TextField(
        verbose_name=_('Message'),
        max_length=255,
        null=False,
        blank=False,
    )

    publication_date = models.DateTimeField(
        verbose_name=_('Publication date'),
        auto_now_add=True,
    )

    owner = models.ForeignKey(settings.AUTH_USER_MODEL,
                              on_delete=models.PROTECT,
                              null=True,
                              verbose_name=_('Owner'))

    content = models.ForeignKey('books.book',
                                on_delete=models.PROTECT,
                                null=False,
                                verbose_name=_('Content'))

    parent = TreeForeignKey(
        'self',
        null=True,
        blank=True,
        related_name="sub_comment",
        on_delete=models.PROTECT,
    )

    # upvotes=models.ManyToManyField(
    #     settings.AUTH_USER_MODEL,
    #     blank=False,
    #     verbose_name=_('Up Votes')
    # )

    # downvotes=models.ManyToManyField(
    #     settings.AUTH_USER_MODEL,
    #     blank=False,
    #     verbose_name=_('Down Votes')
    # )

    # objects = models.Manager()
    # tree = TreeManager()

    class Meta:
        ordering = ('publication_date', )

    # def get_score(self) -> str:
    #     """
    #     Return la différence entre nombre d'up et de down votes
    #     """
    #     return self.upvotes.count() - self.downvotes.count()

    def get_all_children(self, include_self=False):
        """
        Gets all of the comment thread.
        """
        children_list = self._recurse_for_children(self)
        if include_self:
            ix = 0
        else:
            ix = 1
        flat_list = self._flatten(children_list[ix:])
        return flat_list

    def _recurse_for_children(self, node):
        children = []
        children.append(node)
        for child in node.sub_comment.enabled():
            if child != self:
                children_list = self._recurse_for_children(child)
                children.append(children_list)
        return children

    def _flatten(self, L):
        if type(L) != type([]): return [L]
        if L == []: return L
        return self._flatten(L[0]) + self._flatten(L[1:])
예제 #19
0
class Task(TimeStampedModel, MPTTModel):
    """
    Defines the activities of the Work Breakdown Structure (WBS). The WBS is
    the process of subdividing project deliverables and project work into
    smaller, more manageable components.
    """
    NEW = 0
    STARTED = 1
    CLOSED = 2

    TASK_STATUS_CHOICES = [
        (NEW, 'Not Started'),
        (STARTED, 'In Progress'),
        (CLOSED, 'Completed'),
    ]

    name = models.CharField(
        max_length=255, null=False, blank=False,
        verbose_name=_("name"))
    slug = models.SlugField(
        max_length=255, null=False, blank=True,
        verbose_name=_("slug"))
    description = models.TextField(
        _('description'), blank=True,
        help_text=_('Description of activity or task.'))

    project = models.ForeignKey(
        Project, on_delete=models.CASCADE,
        null=False, blank=False,
        related_name="tasks", verbose_name=_("project"))

    resources = models.ManyToManyField(
        Stakeholder, blank=False,
        related_name="tasks",
        verbose_name=_("resources"))

    process = models.ForeignKey(
        Process, on_delete=models.SET_NULL,
        null=True, blank=True,
        help_text=_('PMI process for this task.')
    )

    # Related activities
    parent = TreeForeignKey(
        'self',
        related_name='children',
        null=True, blank=True,
        on_delete=models.SET_NULL,
        verbose_name=_('parent activity'))

    predecessor = TreeForeignKey(
        'self',
        related_name='predecessors',
        null=True, blank=True,
        on_delete=models.SET_NULL)

    successor = TreeForeignKey(
        'self',
        related_name='successors',
        null=True, blank=True,
        on_delete=models.SET_NULL)

    order = models.IntegerField(
        default=10, null=False, blank=False,
        verbose_name=_("order"))

    # Task status
    status = models.CharField(
        max_length=7,
        null=True, blank=True,
        choices=TASK_STATUS_CHOICES,
        default=NEW,
    )
    is_milestone = models.BooleanField(
        default=False, null=False, blank=True,
        verbose_name=_("is milestone"))

    objects = TreeManager()

    class Meta:
        """ Category's meta informations. """
        verbose_name = "task"
        ordering = ["project", "order", "name"]
        unique_together = (("project", "name"), ("project", "slug"))
        ordering = ["project", "created"]
        permissions = (
            ("view_milestone", "Can view milestone"),
        )

    class MPTTMeta:
        """ Category MPTT's meta informations. """
        order_insertion_by = ['name']

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<Milestone {0}>".format(self.id)

    def clean(self):
        # Don't allow draft entries to have a pub_date.
        if self.estimated_start and self.estimated_finish and self.estimated_start > self.estimated_finish:
            raise ValidationError(_('The estimated start must be previous to the estimated finish.'))

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify_uniquely(self.name, self.__class__)
        qs = self.project.tasks
        if self.id:
            qs = qs.exclude(id=self.id)

        self.slug = slugify_uniquely_for_queryset(self.name, qs)
        return super().save(*args, **kwargs)

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

    def get_absolute_url(self):
        """
        Builds and returns the activity's URL
        based on its tree path.
        """
        return reverse('projects:task_detail', args=(self.tree_path,))
예제 #20
0
class BaseLesson(MPTTModel):
    """
    Base Lessons is an abstract model and a blueprint for both
    Lessons an ClassLessons. It depends on course.
    """
    parent = TreeForeignKey(
        'self',
        verbose_name="einhängen unter",
        null=True,
        blank=True,
        related_name='children',
        db_index=True,
    )

    # dependant on course
    course = models.ForeignKey(Course, on_delete=models.PROTECT)

    nr = models.IntegerField(verbose_name=_('Nr.'),
                             help_text=_('steuert nur die Anzeigereihenfolge'),
                             default=1)
    icon = IconField(
        verbose_name="Icon",
        help_text="Neben dem Menüeintrag kann ein Icon angezeigt werden.")
    # this field is derived from nr and and parent.nr depending on the level
    # see save_method
    lesson_nr = models.CharField(
        verbose_name=_('Lektionsnr.'),
        help_text='abgeleitetes Feld: keine manuelle Eingabe',
        blank=True,
        max_length=10,
        editable=False)
    title = models.CharField(verbose_name="Überschrift",
                             help_text="Lektions-Titel",
                             max_length=100)
    text = models.TextField(verbose_name="Lektionstext",
                            help_text="Text der Lektion",
                            blank=True)
    description = models.CharField(
        verbose_name='Beschreibung',
        help_text="diese Beschreibung erscheint nur in den Übersichten",
        max_length=200,
        blank=True)

    material = models.ForeignKey(Material,
                                 verbose_name="Kursmaterial",
                                 help_text="Material der Lektion",
                                 blank=True,
                                 null=True)
    show_number = models.BooleanField(default=True,
                                      verbose_name="Nr. ist Lektionsnummer")
    is_homework = models.BooleanField(default=False)
    allow_questions = models.BooleanField(default=False)
    show_work_area = models.BooleanField(default=False)

    objects = BaseLessonManager()

    # lesson type: blocks > lesson > step having levels: 0, 1, 2
    #LESSON_TYPE = Choices(
    #                 ('block', _('Block')),
    #                 ('lesson', _('Lesson')),
    #                 ('step', _('Step')),)

    class Meta:
        verbose_name = "Lektion"
        verbose_name_plural = "Lektionen"
        abstract = True

    class MPTTMeta:
        order_insertion_by = ['course', 'nr']

    def __unicode__(self):
        """
        blocks are just shown by their title,
        lessons are shown as <block-title>: <lesson_nr>. <lesson_title>
        and lessonsteps are shown as <lesson_nr>. <lesson_title>
        :return: self representation
        """
        if self.level == 0:
            return u'Wurzel: %s' % (self.course)
        elif self.level == 1:
            return u'Block: %s' % (self.title)
        elif self.level == 2:
            return u'%s: %s. %s' % (self.parent.title, self.lesson_nr,
                                    self.title)
        elif self.level == 3:
            return u'%s %s' % (self.lesson_nr, self.title)

    def save(self, *args, **kwargs):
        """
        lesson_nr is calculated and stored in the database for performance
        """
        if self.level:
            if self.level == 1:
                self.lesson_nr = lesson_nr_block(nr=self.nr)
            elif self.level == 2:
                self.lesson_nr = lesson_nr_lesson(nr=self.nr)
            elif self.level == 3:
                self.lesson_nr = lesson_nr_step(nr=self.nr,
                                                parent_nr=self.parent.nr)
        super(BaseLesson, self).save(*args, **kwargs)

    #@property
    #def lesson_type(self):
    #    """
    #    this just translates the internal level of the lesson into a level type,
    #    see above.
    #    :return: lesson_type: block, lesson or step
    #    """
    #    if self.level == 1 :
    #        return self.LESSON_TYPE.block
    #    elif self.level == 2 :
    #        return self.LESSON_TYPE.lesson
    #    elif self.level == 3 :
    #        return self.LESSON_TYPE.step

    @property
    def lesson_type(self):
        """
        this just translates the internal level of the lesson into a level type,
        see above.
        :return: lesson_type: block, lesson or step
        """
        if self.level == 0:
            return LessonType.ROOT
        elif self.level == 1:
            return LessonType.BLOCK
        elif self.level == 2:
            return LessonType.LESSON
        elif self.level == 3:
            return LessonType.LESSONSTEP

    def breadcrumb(self):
        """
        in breadcrumbs a different representation is chosen for block. lesson and step
        block: <title>
        lesson: <lesson_nr>. <title>
        lessonstep: <lesson_nr> <title>
        :return: representation in breadcrumbs
        """
        if self.level == 1 or not self.show_number:
            return u'%s' % (self.title)
        elif self.level == 2:
            return u'%s. %s' % (self.lesson_nr, self.title)
        elif self.level == 3:
            return u'%s %s' % (self.lesson_nr, self.title)

    def get_delete_tree(self):
        return self.get_descendants(
            include_self=True).select_related('material')

    def get_next_sibling(self):
        """
        for blocks the mptt method get_next_sibling needs to be overwritten, so that
        it stays into the course
        :return: None or next mptt-sibiling
        """
        next = super(BaseLesson, self).get_next_sibling()
        try:
            if next.course_id == self.course_id:
                return next
            else:
                return None
        except:
            return None

    def get_previous_sibling(self):
        """
        for blocks the mptt method get_previous_sibling needs to be overwritten, so that
        it stays into the course
        :return: None or previous mptt-sibiling
        """
        previous = super(BaseLesson, self).get_previous_sibling()
        try:
            if previous.course_id == self.course_id:
                return previous
            else:
                return None
        except:
            return None

    def get_breadcrumbs_with_self(self):
        return self.get_ancestors(include_self=True).exclude(level=0)

    def get_tree_with_self_with_material(self):
        """
        gets real decendants and materials of a node
        :return: real decendants with material
        """
        return self.get_descendants(
            include_self=True).select_related('material')

    def get_tree_without_self_with_material(self):
        """
        gets real decendants and materials of a node
        :return: real decendants with material
        """
        return self.get_descendants(
            include_self=False).select_related('material')

    def get_tree_without_self_without_material(self):
        """
        gets real decendants but not the materials of a node
        :return: real decendants without material
        """
        return self.get_descendants(include_self=False)

    def is_block(self):
        """
        bool that decides wether a lesson is a block
        :return: bool
        """
        if self.get_level() == 1:
            return True
        else:
            return False

    def is_lesson(self):
        """
        bool that decides wether a lesson is a lesson
        :return: bool
        """
        if self.get_level() == 2:
            return True
        else:
            return False

    def is_step(self):
        """
        bool that decides wether a lesson is a step
        :return: bool
        """
        if self.get_level() == 3:
            return True
        else:
            return False

    def is_owner(self, user):
        """
        Is this person a teacher in this course? (Then he may work on it.)
        :param user
        :return: bool
        """
        if self.course.is_owner(user):
            return True
        else:
            return False
예제 #21
0
class Category(MPTTModel, TranslatableModel):
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            verbose_name=_('parent category'),
                            on_delete=models.CASCADE)
    shops = models.ManyToManyField("Shop",
                                   blank=True,
                                   related_name="categories",
                                   verbose_name=_("shops"))
    identifier = InternalIdentifierField(unique=True)
    status = EnumIntegerField(CategoryStatus,
                              db_index=True,
                              verbose_name=_('status'),
                              default=CategoryStatus.INVISIBLE)
    image = FilerImageField(verbose_name=_('image'),
                            blank=True,
                            null=True,
                            on_delete=models.SET_NULL)
    ordering = models.IntegerField(default=0, verbose_name=_('ordering'))
    visibility = EnumIntegerField(CategoryVisibility,
                                  db_index=True,
                                  default=CategoryVisibility.VISIBLE_TO_ALL,
                                  verbose_name=_('visibility limitations'))
    visibility_groups = models.ManyToManyField(
        "ContactGroup",
        blank=True,
        verbose_name=_('visible for groups'),
        related_name=u"visible_categories")

    translations = TranslatedFields(
        name=models.CharField(max_length=128, verbose_name=_('name')),
        description=models.TextField(verbose_name=_('description'),
                                     blank=True),
        slug=models.SlugField(blank=True, null=True, verbose_name=_('slug')))

    objects = CategoryManager()

    class Meta:
        ordering = ('tree_id', 'lft')
        verbose_name = _('category')
        verbose_name_plural = _('categories')

    class MPTTMeta:
        order_insertion_by = ["ordering"]

    def __str__(self):
        return self.safe_translation_getter("name", any_language=True)

    def is_visible(self, customer):
        if customer and customer.is_all_seeing:
            return (self.status != CategoryStatus.DELETED)
        if self.status != CategoryStatus.VISIBLE:
            return False
        if not customer or customer.is_anonymous:
            if self.visibility != CategoryVisibility.VISIBLE_TO_ALL:
                return False
        else:
            if self.visibility == CategoryVisibility.VISIBLE_TO_GROUPS:
                group_ids = customer.groups.all().values_list("id", flat=True)
                return self.visibility_groups.filter(id__in=group_ids).exists()
        return True

    @staticmethod
    def _get_slug_name(self, translation):
        if self.status == CategoryStatus.DELETED:
            return None
        return getattr(translation, "name", self.pk)

    def delete(self, using=None):
        raise NotImplementedError(
            "Not implemented: Use `soft_delete()` for categories.")

    @atomic
    def soft_delete(self, user=None):
        if not self.status == CategoryStatus.DELETED:
            for shop_product in self.primary_shop_products.all():
                shop_product.primary_category = None
                shop_product.save()
            for shop_product in self.shop_products.all():
                shop_product.categories.remove(self)
                shop_product.save()
            for product in self.primary_products.all():
                product.category = None
                product.save()
            for child in self.children.all():
                child.parent = None
                child.save()
            self.status = CategoryStatus.DELETED
            self.add_log_entry("Deleted.",
                               kind=LogEntryKind.DELETION,
                               user=user)
            self.save()
            category_deleted.send(sender=type(self), category=self)

    def save(self, *args, **kwargs):
        rv = super(Category, self).save(*args, **kwargs)
        generate_multilanguage_slugs(self, self._get_slug_name)
        return rv
예제 #22
0
class InventoryItem(MPTTModel, ComponentModel):
    """
    An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
    InventoryItems are used only for inventory purposes.
    """
    parent = TreeForeignKey(to='self',
                            on_delete=models.CASCADE,
                            related_name='child_items',
                            blank=True,
                            null=True,
                            db_index=True)
    manufacturer = models.ForeignKey(to='dcim.Manufacturer',
                                     on_delete=models.PROTECT,
                                     related_name='inventory_items',
                                     blank=True,
                                     null=True)
    part_id = models.CharField(
        max_length=50,
        verbose_name='Part ID',
        blank=True,
        help_text='Manufacturer-assigned part identifier')
    serial = models.CharField(max_length=50,
                              verbose_name='Serial number',
                              blank=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')

    tags = TaggableManager(through=TaggedItem)

    objects = TreeManager()

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

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

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

    def to_csv(self):
        return (
            self.device.name or '{{{}}}'.format(self.device.pk),
            self.name,
            self.label,
            self.manufacturer.name if self.manufacturer else None,
            self.part_id,
            self.serial,
            self.asset_tag,
            self.discovered,
            self.description,
        )
예제 #23
0
class ContentNode(MPTTModel):
    """
    The top layer of the contentDB schema, defines the most common properties that are shared across all different contents.
    Things it can represent are, for example, video, exercise, audio or document...
    """
    id = UUIDField(primary_key=True)
    parent = TreeForeignKey('self',
                            null=True,
                            blank=True,
                            related_name='children',
                            db_index=True)
    license_name = models.CharField(max_length=50, null=True, blank=True)
    license_description = models.CharField(max_length=400,
                                           null=True,
                                           blank=True)
    has_prerequisite = models.ManyToManyField('self',
                                              related_name='prerequisite_for',
                                              symmetrical=False,
                                              blank=True)
    related = models.ManyToManyField('self', symmetrical=True, blank=True)
    tags = models.ManyToManyField(ContentTag,
                                  symmetrical=False,
                                  related_name='tagged_content',
                                  blank=True)

    title = models.CharField(max_length=200)

    # the content_id is used for tracking a user's interaction with a piece of
    # content, in the face of possibly many copies of that content. When a user
    # interacts with a piece of content, all substantially similar pieces of
    # content should be marked as such as well. We track these "substantially
    # similar" types of content by having them have the same content_id.
    content_id = UUIDField(db_index=True)
    channel_id = UUIDField(db_index=True)

    description = models.CharField(max_length=400, blank=True, null=True)
    sort_order = models.FloatField(blank=True, null=True)
    license_owner = models.CharField(max_length=200, blank=True)
    author = models.CharField(max_length=200, blank=True)
    kind = models.CharField(max_length=200,
                            choices=content_kinds.choices,
                            blank=True)
    available = models.BooleanField(default=False)
    stemmed_metaphone = models.CharField(
        max_length=1800,
        blank=True)  # for fuzzy search in title and description
    lang = models.ForeignKey('Language', blank=True, null=True)
    coach_content = models.BooleanField(default=False, db_index=True)

    # Added legacy fields
    license = models.ForeignKey('License', null=True, blank=True)

    # A JSON Dictionary of properties to configure loading, rendering, etc. the file
    options = JSONField(default={})

    class Meta:
        ordering = ('lft', )
        index_together = [
            ["level", "channel_id", "kind"],
            ["level", "channel_id", "available"],
        ]

    def __str__(self):
        return self.title

    def get_descendant_content_ids(self):
        """
        Retrieve a queryset of content_ids for non-topic content nodes that are
        descendants of this node.
        """
        return ContentNode.objects \
            .filter(lft__gte=self.lft, lft__lte=self.rght) \
            .exclude(kind=content_kinds.TOPIC) \
            .values_list("content_id", flat=True)
예제 #24
0
파일: models.py 프로젝트: maruhan2/Webfront
class Article(MPTTModel):
    LEVEL_TO_HEADER_MAP = {
        0: "",
        1: "Article",
        2: "Section",
        3: "",
    }

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

    time_created = models.DateTimeField(default=datetime.datetime.now)

    modified_by = models.ManyToManyField(User, through=Modification)

    number = models.IntegerField(null=True, blank=True)
    title = models.CharField(
        max_length=100,
        default="",
        blank=True,
    )
    slug = models.SlugField(null=True, blank=True, max_length=150)
    body = models.TextField(
        default="",
        blank=True,
    )

    objects = models.Manager()
    documents = DocumentManager()

    class Meta:
        verbose_name = _('Article')
        verbose_name_plural = _('Articles')
        unique_together = (('number', 'title', 'level', 'parent'), )

    def __unicode__(self):
        if self.parent is not None:
            if self.title != "":
                return u"{} {}: {}".format(
                    self.LEVEL_TO_HEADER_MAP[self.level], self.number,
                    self.title)
            return u"{}: {}".format(self.number, self.body)
        else:
            return u"{}".format(self.title)

    def save(self, *args, **kwargs):
        self.time_modified = pytz.utc.localize(datetime.datetime.now())
        if not self.slug:
            self.slug = self.title.replace(' ', '-').lower()
        super(Article, self).save(*args, **kwargs)

    def get_time_last_updated(self):
        """Exploits the tree-like structure of a legal document to find the time
        each section was modified - which is distinct from the time that a section
        itself was modified."""
        if len(self.article_set.all()) == 0:
            return max(map(lambda x: x.time, self.modification_set.all()))

        else:
            return max(
                map(lambda x: x.get_time_last_updated(),
                    self.article_set.all()))

    @models.permalink
    def get_absolute_url(self):

        return ('legal_document_detail', {
            'slug': self.slug,
        })
예제 #25
0
class Folder(MPTTModel):
    TRASH_NAME = u'Trash'
    MEETINGS_NAME = u'Meeting Documents'
    COMMITTEES_NAME = u'Committee Documents'
    MEMBERSHIPS_NAME = u'Member Documents'
    RESERVED_NAMES = (TRASH_NAME, MEETINGS_NAME, COMMITTEES_NAME, MEMBERSHIPS_NAME)

    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)

    objects = FolderManager()

    class MPTTMeta:
        order_insertion_by = ('name',)

    class Meta:
        unique_together = (('parent', 'name'),)
        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)
예제 #26
0
class StockItem(models.Model):
    """
    A StockItem object represents a quantity of physical instances of a part.
    
    Attributes:
        part: Link to the master abstract part that this StockItem is an instance of
        supplier_part: Link to a specific SupplierPart (optional)
        location: Where this StockItem is located
        quantity: Number of stocked units
        batch: Batch number for this StockItem
        serial: Unique serial number for this StockItem
        URL: Optional URL to link to external resource
        updated: Date that this stock item was last updated (auto)
        stocktake_date: Date of last stocktake for this item
        stocktake_user: User that performed the most recent stocktake
        review_needed: Flag if StockItem needs review
        delete_on_deplete: If True, StockItem will be deleted when the stock level gets to zero
        status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus)
        notes: Extra notes field
        build: Link to a Build (if this stock item was created from a build)
        purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder)
        infinite: If True this StockItem can never be exhausted
    """

    def save(self, *args, **kwargs):
        if not self.pk:
            add_note = True
        else:
            add_note = False

        user = kwargs.pop('user', None)
        
        add_note = add_note and kwargs.pop('note', True)

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

        if add_note:
            # This StockItem is being saved for the first time
            self.addTransactionNote(
                'Created stock item',
                user,
                notes="Created new stock item for part '{p}'".format(p=str(self.part)),
                system=True
            )

    @property
    def status_label(self):

        return StockStatus.label(self.status)

    @property
    def serialized(self):
        """ Return True if this StockItem is serialized """
        return self.serial is not None and self.quantity == 1

    @classmethod
    def check_serial_number(cls, part, serial_number):
        """ Check if a new stock item can be created with the provided part_id

        Args:
            part: The part to be checked
        """

        if not part.trackable:
            return False

        # Return False if an invalid serial number is supplied
        try:
            serial_number = int(serial_number)
        except ValueError:
            return False

        items = StockItem.objects.filter(serial=serial_number)

        # Is this part a variant? If so, check S/N across all sibling variants
        if part.variant_of is not None:
            items = items.filter(part__variant_of=part.variant_of)
        else:
            items = items.filter(part=part)

        # An existing serial number exists
        if items.exists():
            return False

        return True

    def validate_unique(self, exclude=None):
        super(StockItem, self).validate_unique(exclude)

        # If the Part object is a variant (of a template part),
        # ensure that the serial number is unique
        # across all variants of the same template part

        try:
            if self.serial is not None:
                # This is a variant part (check S/N across all sibling variants)
                if self.part.variant_of is not None:
                    if StockItem.objects.filter(part__variant_of=self.part.variant_of, serial=self.serial).exclude(id=self.id).exists():
                        raise ValidationError({
                            'serial': _('A stock item with this serial number already exists for template part {part}'.format(part=self.part.variant_of))
                        })
                else:
                    if StockItem.objects.filter(part=self.part, serial=self.serial).exclude(id=self.id).exists():
                        raise ValidationError({
                            'serial': _('A stock item with this serial number already exists')
                        })
        except Part.DoesNotExist:
            pass

    def clean(self):
        """ Validate the StockItem object (separate to field validation)

        The following validation checks are performed:

        - The 'part' and 'supplier_part.part' fields cannot point to the same Part object
        - The 'part' does not belong to itself
        - Quantity must be 1 if the StockItem has a serial number
        """

        # The 'supplier_part' field must point to the same part!
        try:
            if self.supplier_part is not None:
                if not self.supplier_part.part == self.part:
                    raise ValidationError({'supplier_part': _("Part type ('{pf}') must be {pe}").format(
                                           pf=str(self.supplier_part.part),
                                           pe=str(self.part))
                                           })

            if self.part is not None:
                # A part with a serial number MUST have the quantity set to 1
                if self.serial is not None:
                    if self.quantity > 1:
                        raise ValidationError({
                            'quantity': _('Quantity must be 1 for item with a serial number'),
                            'serial': _('Serial number cannot be set if quantity greater than 1')
                        })

                    if self.quantity == 0:
                        self.quantity = 1

                    elif self.quantity > 1:
                        raise ValidationError({
                            'quantity': _('Quantity must be 1 for item with a serial number')
                        })

                    # Serial numbered items cannot be deleted on depletion
                    self.delete_on_deplete = False

                # A template part cannot be instantiated as a StockItem
                if self.part.is_template:
                    raise ValidationError({'part': _('Stock item cannot be created for a template Part')})

        except Part.DoesNotExist:
            # This gets thrown if self.supplier_part is null
            # TODO - Find a test than can be perfomed...
            pass

        if self.belongs_to and self.belongs_to.pk == self.pk:
            raise ValidationError({
                'belongs_to': _('Item cannot belong to itself')
            })

    def get_absolute_url(self):
        return reverse('stock-item-detail', kwargs={'pk': self.id})

    def get_part_name(self):
        return self.part.full_name

    class Meta:
        unique_together = [
            ('part', 'serial'),
        ]

    def format_barcode(self):
        """ Return a JSON string for formatting a barcode for this StockItem.
        Can be used to perform lookup of a stockitem using barcode

        Contains the following data:

        { type: 'StockItem', stock_id: <pk>, part_id: <part_pk> }

        Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
        """

        return helpers.MakeBarcode(
            'StockItem',
            self.id,
            reverse('api-stock-detail', kwargs={'pk': self.id}),
            {
                'part_id': self.part.id,
                'part_name': self.part.full_name
            }
        )

    part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
                             related_name='stock_items', help_text=_('Base part'),
                             limit_choices_to={
                                 'is_template': False,
                                 'active': True,
                             })

    supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
                                      help_text=_('Select a matching supplier part for this stock item'))

    location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING,
                              related_name='stock_items', blank=True, null=True,
                              help_text=_('Where is this stock item located?'))

    belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
                                   related_name='owned_parts', blank=True, null=True,
                                   help_text=_('Is this item installed in another item?'))

    customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL,
                                 related_name='stockitems', blank=True, null=True,
                                 help_text=_('Item assigned to customer?'))

    serial = models.PositiveIntegerField(blank=True, null=True,
                                         help_text=_('Serial number for this item'))
 
    URL = InvenTreeURLField(max_length=125, blank=True)

    batch = models.CharField(max_length=100, blank=True, null=True,
                             help_text=_('Batch code for this stock item'))

    quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)

    updated = models.DateField(auto_now=True, null=True)

    build = models.ForeignKey(
        'build.Build', on_delete=models.SET_NULL,
        blank=True, null=True,
        help_text=_('Build for this stock item'),
        related_name='build_outputs',
    )

    purchase_order = models.ForeignKey(
        'order.PurchaseOrder',
        on_delete=models.SET_NULL,
        related_name='stock_items',
        blank=True, null=True,
        help_text=_('Purchase order for this stock item')
    )

    # last time the stock was checked / counted
    stocktake_date = models.DateField(blank=True, null=True)

    stocktake_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True,
                                       related_name='stocktake_stock')

    review_needed = models.BooleanField(default=False)

    delete_on_deplete = models.BooleanField(default=True, help_text=_('Delete this Stock Item when stock is depleted'))

    status = models.PositiveIntegerField(
        default=StockStatus.OK,
        choices=StockStatus.items(),
        validators=[MinValueValidator(0)])

    notes = models.CharField(max_length=250, blank=True, help_text=_('Stock Item Notes'))

    # If stock item is incoming, an (optional) ETA field
    # expected_arrival = models.DateField(null=True, blank=True)

    infinite = models.BooleanField(default=False)

    def can_delete(self):
        """ Can this stock item be deleted? It can NOT be deleted under the following circumstances:

        - Has a serial number and is tracked
        - Is installed inside another StockItem
        """

        if self.part.trackable and self.serial is not None:
            return False

        return True

    @property
    def in_stock(self):

        if self.belongs_to or self.customer:
            return False

        return True

    @property
    def has_tracking_info(self):
        return self.tracking_info.count() > 0

    def addTransactionNote(self, title, user, notes='', url='', system=True):
        """ Generation a stock transaction note for this item.

        Brief automated note detailing a movement or quantity change.
        """
        
        track = StockItemTracking.objects.create(
            item=self,
            title=title,
            user=user,
            quantity=self.quantity,
            date=datetime.now().date(),
            notes=notes,
            URL=url,
            system=system
        )

        track.save()

    @transaction.atomic
    def serializeStock(self, quantity, serials, user, notes='', location=None):
        """ Split this stock item into unique serial numbers.

        - Quantity can be less than or equal to the quantity of the stock item
        - Number of serial numbers must match the quantity
        - Provided serial numbers must not already be in use

        Args:
            quantity: Number of items to serialize (integer)
            serials: List of serial numbers (list<int>)
            user: User object associated with action
            notes: Optional notes for tracking
            location: If specified, serialized items will be placed in the given location
        """

        # Cannot serialize stock that is already serialized!
        if self.serialized:
            return

        # Quantity must be a valid integer value
        try:
            quantity = int(quantity)
        except ValueError:
            raise ValidationError({"quantity": _("Quantity must be integer")})

        if quantity <= 0:
            raise ValidationError({"quantity": _("Quantity must be greater than zero")})

        if quantity > self.quantity:
            raise ValidationError({"quantity": _("Quantity must not exceed available stock quantity ({n})".format(n=self.quantity))})

        if not type(serials) in [list, tuple]:
            raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})

        if any([type(i) is not int for i in serials]):
            raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})

        if not quantity == len(serials):
            raise ValidationError({"quantity": _("Quantity does not match serial numbers")})

        # Test if each of the serial numbers are valid
        existing = []

        for serial in serials:
            if not StockItem.check_serial_number(self.part, serial):
                existing.append(serial)

        if len(existing) > 0:
            raise ValidationError({"serial_numbers": _("Serial numbers already exist: ") + str(existing)})

        # Create a new stock item for each unique serial number
        for serial in serials:
            
            # Create a copy of this StockItem
            new_item = StockItem.objects.get(pk=self.pk)
            new_item.quantity = 1
            new_item.serial = serial
            new_item.pk = None

            if location:
                new_item.location = location

            # The item already has a transaction history, don't create a new note
            new_item.save(user=user, note=False)

            # Copy entire transaction history
            new_item.copyHistoryFrom(self)

            # Create a new stock tracking item
            new_item.addTransactionNote(_('Add serial number'), user, notes=notes)

        # Remove the equivalent number of items
        self.take_stock(quantity, user, notes=_('Serialized {n} items'.format(n=quantity)))

    @transaction.atomic
    def copyHistoryFrom(self, other):
        """ Copy stock history from another part """

        for item in other.tracking_info.all():
            
            item.item = self
            item.pk = None
            item.save()

    @transaction.atomic
    def splitStock(self, quantity, user):
        """ Split this stock item into two items, in the same location.
        Stock tracking notes for this StockItem will be duplicated,
        and added to the new StockItem.

        Args:
            quantity: Number of stock items to remove from this entity, and pass to the next

        Notes:
            The provided quantity will be subtracted from this item and given to the new one.
            The new item will have a different StockItem ID, while this will remain the same.
        """

        # Do not split a serialized part
        if self.serialized:
            return

        try:
            quantity = Decimal(quantity)
        except (InvalidOperation, ValueError):
            return

        # Doesn't make sense for a zero quantity
        if quantity <= 0:
            return

        # Also doesn't make sense to split the full amount
        if quantity >= self.quantity:
            return

        # Create a new StockItem object, duplicating relevant fields
        # Nullify the PK so a new record is created
        new_stock = StockItem.objects.get(pk=self.pk)
        new_stock.pk = None
        new_stock.quantity = quantity
        new_stock.save()

        # Copy the transaction history of this part into the new one
        new_stock.copyHistoryFrom(self)

        # Add a new tracking item for the new stock item
        new_stock.addTransactionNote(
            "Split from existing stock",
            user,
            "Split {n} from existing stock item".format(n=quantity))

        # Remove the specified quantity from THIS stock item
        self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity))

    @transaction.atomic
    def move(self, location, notes, user, **kwargs):
        """ Move part to a new location.

        Args:
            location: Destination location (cannot be null)
            notes: User notes
            user: Who is performing the move
            kwargs:
                quantity: If provided, override the quantity (default = total stock quantity)
        """

        try:
            quantity = Decimal(kwargs.get('quantity', self.quantity))
        except InvalidOperation:
            return False

        if quantity <= 0:
            return False

        if location is None:
            # TODO - Raise appropriate error (cannot move to blank location)
            return False
        elif self.location and (location.pk == self.location.pk) and (quantity == self.quantity):
            # TODO - Raise appropriate error (cannot move to same location)
            return False

        # Test for a partial movement
        if quantity < self.quantity:
            # We need to split the stock!

            # Leave behind certain quantity
            self.splitStock(self.quantity - quantity, user)

        msg = "Moved to {loc}".format(loc=str(location))

        if self.location:
            msg += " (from {loc})".format(loc=str(self.location))

        self.location = location

        self.addTransactionNote(msg,
                                user,
                                notes=notes,
                                system=True)

        self.save()

        return True

    @transaction.atomic
    def updateQuantity(self, quantity):
        """ Update stock quantity for this item.
        
        If the quantity has reached zero, this StockItem will be deleted.

        Returns:
            - True if the quantity was saved
            - False if the StockItem was deleted
        """

        # Do not adjust quantity of a serialized part
        if self.serialized:
            return

        try:
            self.quantity = Decimal(quantity)
        except (InvalidOperation, ValueError):
            return

        if quantity < 0:
            quantity = 0

        self.quantity = quantity

        if quantity == 0 and self.delete_on_deplete and self.can_delete():
            
            # TODO - Do not actually "delete" stock at this point - instead give it a "DELETED" flag
            self.delete()
            return False
        else:
            self.save()
            return True

    @transaction.atomic
    def stocktake(self, count, user, notes=''):
        """ Perform item stocktake.
        When the quantity of an item is counted,
        record the date of stocktake
        """

        try:
            count = Decimal(count)
        except InvalidOperation:
            return False

        if count < 0 or self.infinite:
            return False

        self.stocktake_date = datetime.now().date()
        self.stocktake_user = user

        if self.updateQuantity(count):

            self.addTransactionNote('Stocktake - counted {n} items'.format(n=count),
                                    user,
                                    notes=notes,
                                    system=True)

        return True

    @transaction.atomic
    def add_stock(self, quantity, user, notes=''):
        """ Add items to stock
        This function can be called by initiating a ProjectRun,
        or by manually adding the items to the stock location
        """

        # Cannot add items to a serialized part
        if self.serialized:
            return False

        try:
            quantity = Decimal(quantity)
        except InvalidOperation:
            return False

        # Ignore amounts that do not make sense
        if quantity <= 0 or self.infinite:
            return False

        if self.updateQuantity(self.quantity + quantity):
            
            self.addTransactionNote('Added {n} items to stock'.format(n=quantity),
                                    user,
                                    notes=notes,
                                    system=True)

        return True

    @transaction.atomic
    def take_stock(self, quantity, user, notes=''):
        """ Remove items from stock
        """

        # Cannot remove items from a serialized part
        if self.serialized:
            return False

        try:
            quantity = Decimal(quantity)
        except InvalidOperation:
            return False

        if quantity <= 0 or self.infinite:
            return False

        if self.updateQuantity(self.quantity - quantity):

            self.addTransactionNote('Removed {n} items from stock'.format(n=quantity),
                                    user,
                                    notes=notes,
                                    system=True)

        return True

    def __str__(self):
        if self.part.trackable and self.serial:
            s = '{part} #{sn}'.format(
                part=self.part.full_name,
                sn=self.serial)
        else:
            s = '{n} x {part}'.format(
                n=helpers.decimal2string(self.quantity),
                part=self.part.full_name)

        if self.location:
            s += ' @ {loc}'.format(loc=self.location.name)

        return s
예제 #27
0
class Page(MPTTModel, TranslatableModel):
    shop = models.ForeignKey("shuup.Shop", verbose_name=_('shop'))
    supplier = models.ForeignKey("shuup.Supplier",
                                 null=True,
                                 blank=True,
                                 verbose_name=_('supplier'))
    available_from = models.DateTimeField(
        default=now,
        null=True,
        blank=True,
        db_index=True,
        verbose_name=_('available from'),
        help_text=
        _("Set an available from date to restrict the page to be available only after a certain date and time. "
          "This is useful for pages describing sales campaigns or other time-sensitive pages."
          ))
    available_to = models.DateTimeField(
        null=True,
        blank=True,
        db_index=True,
        verbose_name=_('available to'),
        help_text=
        _("Set an available to date to restrict the page to be available only after a certain date and time. "
          "This is useful for pages describing sales campaigns or other time-sensitive pages."
          ))

    created_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                   blank=True,
                                   null=True,
                                   related_name="+",
                                   on_delete=models.SET_NULL,
                                   verbose_name=_('created by'))
    modified_by = models.ForeignKey(settings.AUTH_USER_MODEL,
                                    blank=True,
                                    null=True,
                                    related_name="+",
                                    on_delete=models.SET_NULL,
                                    verbose_name=_('modified by'))

    created_on = models.DateTimeField(auto_now_add=True,
                                      editable=False,
                                      verbose_name=_('created on'))
    modified_on = models.DateTimeField(auto_now=True,
                                       editable=False,
                                       verbose_name=_('modified on'))

    identifier = InternalIdentifierField(
        unique=False,
        help_text=_('This identifier can be used in templates to create URLs'),
        editable=True)

    visible_in_menu = models.BooleanField(
        verbose_name=_("visible in menu"),
        default=False,
        help_text=
        _("Check this if this page should have a link in the top menu of the store front."
          ))
    parent = TreeForeignKey(
        "self",
        blank=True,
        null=True,
        related_name="children",
        verbose_name=_("parent"),
        help_text=
        _("Set this to a parent page if this page should be subcategorized under another page."
          ))
    list_children_on_page = models.BooleanField(
        verbose_name=_("list children on page"),
        default=False,
        help_text=_("Check this if this page should list its children pages."))
    show_child_timestamps = models.BooleanField(
        verbose_name=_("show child page timestamps"),
        default=True,
        help_text=_(
            "Check this if you want to show timestamps on the child pages. Please note, that this "
            "requires the children to be listed on the page as well."))
    deleted = models.BooleanField(default=False, verbose_name=_("deleted"))

    translations = TranslatedFields(
        title=models.CharField(
            max_length=256,
            verbose_name=_('title'),
            help_text=
            _("The page title. This is shown anywhere links to your page are shown."
              )),
        url=models.CharField(
            max_length=100,
            verbose_name=_('URL'),
            default=None,
            blank=True,
            null=True,
            help_text=
            _("The page url. Choose a descriptive url so that search engines can rank your page higher. "
              "Often the best url is simply the page title with spaces replaced with dashes."
              )),
        content=models.TextField(
            verbose_name=_('content'),
            help_text=
            _("The page content. This is the text that is displayed when customers click on your page link."
              "You can leave this empty and add all page content through placeholder editor in shop front."
              "To edit the style of the page you can use the Snippet plugin which is in shop front editor."
              )))
    template_name = models.TextField(
        max_length=500,
        verbose_name=_("Template path"),
        default=settings.SHUUP_SIMPLE_CMS_DEFAULT_TEMPLATE)
    render_title = models.BooleanField(
        verbose_name=_("render title"),
        default=True,
        help_text=_("Check this if this page should have a visible title"))

    objects = TreeManager.from_queryset(PageQuerySet)()

    class Meta:
        ordering = ('-id', )
        verbose_name = _('page')
        verbose_name_plural = _('pages')
        unique_together = ("shop", "identifier")

    def delete(self, using=None):
        raise NotImplementedError("Not implemented: Use `soft_delete()`")

    def soft_delete(self, user=None):
        if not self.deleted:
            self.deleted = True
            self.add_log_entry("Deleted.",
                               kind=LogEntryKind.DELETION,
                               user=user)
            # Bypassing local `save()` on purpose.
            super(Page, self).save(update_fields=("deleted", ))

    def clean(self):
        url = getattr(self, "url", None)
        if url:
            page_translation = self._meta.model._parler_meta.root_model
            shop_pages = Page.objects.for_shop(self.shop).values_list(
                "id", flat=True)
            url_checker = page_translation.objects.filter(
                url=url, master_id__in=shop_pages)
            if self.pk:
                url_checker = url_checker.exclude(master_id=self.pk)
            if url_checker.exists():
                raise ValidationError(_("URL already exists."),
                                      code="invalid_url")

    def is_visible(self, dt=None):
        if not dt:
            dt = now()

        return ((self.available_from and self.available_from <= dt)
                and (self.available_to is None or self.available_to >= dt))

    def save(self, *args, **kwargs):
        with reversion.create_revision():
            super(Page, self).save(*args, **kwargs)

    def get_html(self):
        return self.content

    @classmethod
    def create_initial_revision(cls, page):
        from reversion.models import Version
        if not Version.objects.get_for_object(page).exists():
            with reversion.create_revision():
                page.save()

    def __str__(self):
        return force_text(
            self.safe_translation_getter("title",
                                         any_language=True,
                                         default=_("Untitled")))
예제 #28
0
class Region(MPTTModel):
    # objects = RegionManager()

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

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

    def __unicode__(self):
        return self.name

    @property
    def bbox(self):
        return [
            self.bbox_x0, self.bbox_y0, self.bbox_x1, 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']
예제 #29
0
class Place(models.Model):
    location = TreeForeignKey('locationstree.Location')
예제 #30
0
class Place(MPTTModel, BaseModel, SchemalessFieldMixin):
    """
        Tavastia Events
        publisher muutettu vapaaehtoiseksi
        position kentän srid muutettu 4326, Google Mapsin käytössä oleva srid. Alkuperäinen löytyy settings.PROJECTION_SRID
        unique_together poistettu käytöstä 
    """
    publisher = models.ForeignKey('django_orghierarchy.Organization',
                                  verbose_name=_('Publisher'),
                                  db_index=True,
                                  null=True,
                                  blank=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)

    geo_objects = models.GeoManager()
    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')
        """
            Tavastia Events
            Paikoille ei ole lähdettä/origin_id
        """
        #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 = AdministrativeDivision.objects.filter(
                type__type__in=('district', 'sub_district', 'neighborhood',
                                'muni'),
                geometry__boundary__contains=self.position)
        else:
            self.divisions.clear()