コード例 #1
0
class Profile(models.Model):
    """
    A user profile. Complementary data of standard Django `auth.user`.
    """
    class Meta:
        verbose_name = 'Profil'
        verbose_name_plural = 'Profils'
        permissions = (
            ('moderation', _('Modérer un membre')),
            ('show_ip', _("Afficher les IP d'un membre")),
        )

    # Link with standard user is a simple one-to-one link, as recommended in official documentation.
    # See https://docs.djangoproject.com/en/1.6/topics/auth/customizing/#extending-the-existing-user-model
    user = models.OneToOneField(User,
                                verbose_name='Utilisateur',
                                related_name='profile')

    last_ip_address = models.CharField('Adresse IP',
                                       max_length=39,
                                       blank=True,
                                       null=True)

    site = models.CharField('Site internet', max_length=2000, blank=True)
    show_email = models.BooleanField('Afficher adresse mail publiquement',
                                     default=False)
    avatar_url = models.CharField('URL de l\'avatar',
                                  max_length=2000,
                                  null=True,
                                  blank=True)
    biography = models.TextField('Biographie', blank=True)
    karma = models.IntegerField('Karma', default=0)
    sign = models.TextField('Signature', max_length=500, blank=True)
    licence = models.ForeignKey(Licence,
                                verbose_name='Licence préférée',
                                blank=True,
                                null=True)
    github_token = models.TextField('GitHub', blank=True)
    show_sign = models.BooleanField('Voir les signatures', default=True)
    # do UI components open by hovering them, or is clicking on them required?
    is_hover_enabled = models.BooleanField('Déroulement au survol ?',
                                           default=False)
    allow_temp_visual_changes = models.BooleanField(
        'Activer les changements visuels temporaires', default=True)
    show_markdown_help = models.BooleanField(
        "Afficher l'aide Markdown dans l'éditeur", default=True)
    email_for_answer = models.BooleanField('Envoyer pour les réponse MP',
                                           default=False)
    email_for_new_mp = models.BooleanField('Envoyer pour les nouveaux MP',
                                           default=False)
    hats = models.ManyToManyField(Hat,
                                  verbose_name='Casquettes',
                                  db_index=True,
                                  blank=True)
    can_read = models.BooleanField('Possibilité de lire', default=True)
    end_ban_read = models.DateTimeField("Fin d'interdiction de lecture",
                                        null=True,
                                        blank=True)
    can_write = models.BooleanField("Possibilité d'écrire", default=True)
    end_ban_write = models.DateTimeField("Fin d'interdiction d'écrire",
                                         null=True,
                                         blank=True)
    last_visit = models.DateTimeField('Date de dernière visite',
                                      null=True,
                                      blank=True)
    use_old_smileys = models.BooleanField('Utilise les anciens smileys ?',
                                          default=False)
    _permissions = {}
    _groups = None

    objects = ProfileManager()

    def __str__(self):
        return self.user.username

    def is_private(self):
        """Can the user display their stats?"""
        user_groups = self.user.groups.all()
        user_group_names = [g.name for g in user_groups]
        return settings.ZDS_APP['member']['bot_group'] in user_group_names

    def get_absolute_url(self):
        """Absolute URL to the profile page."""
        return reverse('member-detail',
                       kwargs={'user_name': self.user.username})

    def get_city(self):
        """
        Uses geo-localization to get physical localization of a profile through its last IP address.
        This works relatively well with IPv4 addresses (~city level), but is very imprecise with IPv6 or exotic internet
        providers.
        :return: The city and the country name of this profile.
        """
        # FIXME: this test to differentiate IPv4 and IPv6 addresses doesn't work, as IPv6 addresses may have length < 16
        # Example: localhost ("::1"). Real test: IPv4 addresses contains dots, IPv6 addresses contains columns.
        if len(self.last_ip_address) <= 16:
            gic = pygeoip.GeoIP(
                os.path.join(settings.GEOIP_PATH, 'GeoLiteCity.dat'))
        else:
            gic = pygeoip.GeoIP(
                os.path.join(settings.GEOIP_PATH, 'GeoLiteCityv6.dat'))

        geo = gic.record_by_addr(self.last_ip_address)

        if geo is None:
            return ''

        city = geo['city']
        country = geo['country_name']
        return ', '.join(i for i in [city, country] if i)

    def get_avatar_url(self):
        """Get the avatar URL for this profile.
        If the user has defined a custom URL, use it.
        If not, use Gravatar.
        :return: The avatar URL for this profile
        :rtype: str
        """
        if self.avatar_url:
            if self.avatar_url.startswith(settings.MEDIA_URL):
                return '{}{}'.format(settings.ZDS_APP['site']['url'],
                                     self.avatar_url)
            else:
                return self.avatar_url
        else:
            return 'https://secure.gravatar.com/avatar/{0}?d=identicon'.format(
                md5(self.user.email.lower().encode('utf-8')).hexdigest())

    def get_post_count(self):
        """
        :return: The forum post count. Doesn't count comments on articles or tutorials.
        """
        return Post.objects.filter(author__pk=self.user.pk,
                                   is_visible=True).count()

    def get_post_count_as_staff(self):
        """Number of messages posted (view as staff)."""

        return Post.objects.filter(author__pk=self.user.pk).count()

    def get_topic_count(self):
        """
        :return: the number of topics created by this user.
        """
        return Topic.objects.filter(author=self.user).count()

    def get_user_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents with this user as author.
        """
        queryset = PublishableContent.objects.filter(authors__in=[self.user])

        if _type:
            queryset = queryset.filter(type=_type)

        return queryset

    def get_user_public_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents with this user as author.
        """
        queryset = PublishedContent.objects.filter(authors__in=[self.user])

        if _type:
            queryset = queryset.filter(content_type=_type)

        return queryset

    def get_content_count(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: the count of contents with this user as author. Count all contents no only published one.
        """
        if self.is_private():
            return 0
        return self.get_user_contents_queryset(_type).count()

    def get_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents with this user as author.
        """
        return self.get_user_contents_queryset(_type).all()

    def get_draft_contents(self, _type=None):
        """Return all draft contents with this user as author.
        A draft content is a content which is not published, in validation or in beta.

        :param _type: if provided, request a specific type of content
        :return: All draft tutorials with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_draft__isnull=False,
            sha_beta__isnull=True,
            sha_validation__isnull=True,
            sha_public__isnull=True).all()

    def get_public_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All published contents with this user as author.
        """
        return self.get_user_public_contents_queryset(_type).filter(
            must_redirect=False).all()

    def get_validate_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents in validation with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_validation__isnull=False).all()

    def get_beta_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All tutorials in beta with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_beta__isnull=False).all()

    def get_tuto_count(self):
        """
        :return: the count of tutorials with this user as author. Count all tutorials, no only published one.
        """
        return self.get_content_count(_type='TUTORIAL')

    def get_tutos(self):
        """
        :return: All tutorials with this user as author.
        """
        return self.get_contents(_type='TUTORIAL')

    def get_draft_tutos(self):
        """
        Return all draft tutorials with this user as author.
        A draft tutorial is a tutorial which is not published, in validation or in beta.
        :return: All draft tutorials with this user as author.
        """
        return self.get_draft_contents(_type='TUTORIAL')

    def get_public_tutos(self):
        """
        :return: All published tutorials with this user as author.
        """
        return self.get_public_contents(_type='TUTORIAL')

    def get_validate_tutos(self):
        """
        :return: All tutorials in validation with this user as author.
        """
        return self.get_validate_contents(_type='TUTORIAL')

    def get_beta_tutos(self):
        """
        :return: All tutorials in beta with this user as author.
        """
        return self.get_beta_contents(_type='TUTORIAL')

    def get_article_count(self):
        """
        :return: the count of articles with this user as author. Count all articles, no only published one.
        """
        return self.get_content_count(_type='ARTICLE')

    def get_articles(self):
        """
        :return: All articles with this user as author.
        """
        return self.get_contents(_type='ARTICLE')

    def get_public_articles(self):
        """
        :return: All published articles with this user as author.
        """
        return self.get_public_contents(_type='ARTICLE')

    def get_validate_articles(self):
        """
        :return: All articles in validation with this user as author.
        """
        return self.get_validate_contents(_type='ARTICLE')

    def get_draft_articles(self):
        """
        Return all draft article with this user as author.
        A draft article is a article which is not published or in validation.
        :return: All draft article with this user as author.
        """
        return self.get_draft_contents(_type='ARTICLE')

    def get_beta_articles(self):
        """
        :return: All articles in beta with this user as author.
        """
        return self.get_beta_contents(_type='ARTICLE')

    def get_opinion_count(self):
        """
        :return: the count of opinions with this user as author. Count all opinions, no only published one.
        """
        return self.get_content_count(_type='OPINION')

    def get_opinions(self):
        """
        :return: All opinions with this user as author.
        """
        return self.get_contents(_type='OPINION')

    def get_public_opinions(self):
        """
        :return: All published opinions with this user as author.
        """
        return self.get_public_contents(_type='OPINION')

    def get_draft_opinions(self):
        """
        Return all draft opinion with this user as author.
        A draft opinion is a opinion which is not published or in validation.
        :return: All draft opinion with this user as author.
        """
        return self.get_draft_contents(_type='OPINION')

    def get_posts(self):
        return Post.objects.filter(author=self.user).all()

    def get_hidden_by_staff_posts_count(self):
        return Post.objects.filter(
            is_visible=False,
            author=self.user).exclude(editor=self.user).count()

    def get_active_alerts_count(self):
        """
        :return: The number of currently active alerts created by this user.
        """
        return Alert.objects.filter(author=self.user, solved=False).count()

    def can_read_now(self):
        if self.user.is_authenticated:
            if self.user.is_active:
                if self.end_ban_read:
                    return self.can_read or (self.end_ban_read <
                                             datetime.now())
                return self.can_read
            return False

    def can_write_now(self):
        if self.user.is_active:
            if self.end_ban_write:
                return self.can_write or (self.end_ban_write < datetime.now())
            return self.can_write
        return False

    def get_followed_topics(self):
        """
        :return: All forum topics followed by this user.
        """
        return Topic.objects.filter(topicfollowed__user=self.user)\
            .order_by('-last_message__pubdate')

    def is_dev(self):
        """
        Checks whether user is part of group `settings.ZDS_APP['member']['dev_group']`.
        """
        return self.user.groups.filter(
            name=settings.ZDS_APP['member']['dev_group']).exists()

    def has_hat(self):
        """
        Checks if this user can at least use one hat.
        """
        return len(self.get_hats()) >= 1

    def get_hats(self):
        """
        Return all hats the user is allowed to use.
        """
        profile_hats = list(self.hats.all())
        groups_hats = list(
            Hat.objects.filter(group__in=self.user.groups.all()))
        hats = profile_hats + groups_hats
        hats.sort(key=lambda hat: hat.name)
        return hats

    def get_requested_hats(self):
        """
        Return all current hats requested by this user.
        """
        return self.user.requested_hats.filter(
            is_granted__isnull=True).order_by('-date')

    def get_solved_hat_requests(self):
        """
        Return old hats requested by this user.
        """
        return self.user.requested_hats.filter(
            is_granted__isnull=False).order_by('-solved_at')

    @staticmethod
    def has_read_permission(request):
        return True

    def has_object_read_permission(self, request):
        return True

    @staticmethod
    def has_write_permission(request):
        return True

    def has_object_write_permission(self, request):
        return self.has_object_update_permission(
            request) or request.user.has_perm('member.change_profile')

    def has_object_update_permission(self, request):
        return request.user.is_authenticated() and request.user == self.user

    @staticmethod
    def has_ban_permission(request):
        return True

    def has_object_ban_permission(self, request):
        return request.user and request.user.has_perm('member.change_profile')

    @property
    def group_pks(self):
        if self._groups is None:
            self._groups = list(self.user.groups.all())
        return [g.pk for g in self._groups]
コード例 #2
0
class Profile(models.Model):
    """
    A user profile. Complementary data of standard Django `auth.user`.
    """
    class Meta:
        verbose_name = 'Profil'
        verbose_name_plural = 'Profils'
        permissions = (
            ("moderation", u"Modérer un membre"),
            ("show_ip", u"Afficher les IP d'un membre"),
        )

    # Link with standard user is a simple one-to-one link, as recommended in official documentation.
    # See https://docs.djangoproject.com/en/1.6/topics/auth/customizing/#extending-the-existing-user-model
    user = models.OneToOneField(User,
                                verbose_name='Utilisateur',
                                related_name="profile")

    last_ip_address = models.CharField('Adresse IP',
                                       max_length=39,
                                       blank=True,
                                       null=True)

    migrated = models.BooleanField(
        'Compte migré après l\'import depuis FluxBB', default=True)

    site = models.CharField('Site internet', max_length=2000, blank=True)
    title = models.CharField('Titre', max_length=250, blank=True)

    show_email = models.BooleanField('Afficher adresse mail publiquement',
                                     default=False)

    avatar_url = models.CharField('URL de l\'avatar',
                                  max_length=2000,
                                  null=True,
                                  blank=True)

    biography = models.TextField('Biographie', blank=True)

    karma = models.IntegerField('Karma', default=0)

    sign = models.TextField('Signature', max_length=500, blank=True)

    show_sign = models.BooleanField('Voir les signatures', default=True)

    # TODO: Change this name. This is a boolean: "true" is "hover" or "click" ?!
    hover_or_click = models.BooleanField('Survol ou click ?', default=False)

    allow_temp_visual_changes = models.BooleanField(
        'Activer les changements visuels temporaires', default=True)

    email_for_answer = models.BooleanField('Envoyer pour les réponse MP',
                                           default=False)

    # SdZ tutorial IDs separated by columns (:).
    # TODO: bad field name (singular --> should be plural), manually handled multi-valued field.
    sdz_tutorial = models.TextField('Identifiant des tutos SdZ',
                                    blank=True,
                                    null=True)

    can_read = models.BooleanField('Possibilité de lire', default=True)
    end_ban_read = models.DateTimeField('Fin d\'interdiction de lecture',
                                        null=True,
                                        blank=True)

    can_write = models.BooleanField('Possibilité d\'écrire', default=True)
    end_ban_write = models.DateTimeField('Fin d\'interdiction d\'ecrire',
                                         null=True,
                                         blank=True)

    last_visit = models.DateTimeField('Date de dernière visite',
                                      null=True,
                                      blank=True)

    objects = ProfileManager()
    _permissions = {}

    def __unicode__(self):
        return self.user.username

    def in_group(self, group_name):
        """
        Checks the user is in the given group
        :param group_name: The group name
        """
        return group_name in [g.name for g in self.user.groups.all()]

    def is_private(self):
        """Checks the user can display his stats"""
        return self.in_group(settings.ZDS_APP['member']['bot_group'])

    def is_god(self):
        """Checks if the user must be displayed as a god"""
        return self.in_group(settings.ZDS_APP['member']['god_group'])

    def is_admin(self):
        """Checks if the user must be displayed as an admin"""
        return self.in_group(settings.ZDS_APP['member']['admin_group'])

    def is_staff(self):
        """Checks if the user must be displayed as a staff member"""
        return self.in_group(settings.ZDS_APP['member']['staff_group'])

    def is_animator(self):
        """Checks if the user must be displayed as an animator"""
        return self.in_group(settings.ZDS_APP['member']['animator_group'])

    def get_absolute_url(self):
        """Absolute URL to the profile page."""
        return reverse('member-detail',
                       kwargs={'user_name': self.user.username})

    def get_city(self):
        """
        Uses geo-localization to get physical localization of a profile through its last IP address.
        This works relatively good with IPv4 addresses (~city level), but is very imprecise with IPv6 or exotic internet
        providers.
        :return: The city and the country name of this profile.
        """
        if self.last_ip_address:
            # FIXME: this test to differentiate IPv4 and IPv6 addresses doesn't work, as IPv6 addresses may have
            # FIXME: length < 16
            # Example: localhost ("::1"). Real test: IPv4 addresses contains dots, IPv6 addresses contains columns.
            if len(self.last_ip_address) <= 16:
                gic = pygeoip.GeoIP(
                    os.path.join(settings.GEOIP_PATH, 'GeoLiteCity.dat'))
            else:
                gic = pygeoip.GeoIP(
                    os.path.join(settings.GEOIP_PATH, 'GeoLiteCityv6.dat'))

            geo = gic.record_by_addr(self.last_ip_address)

            if geo is not None:
                return u'{0}, {1}'.format(geo['city'], geo['country_name'])
        return ''

    def get_avatar_url(self):
        """Get the avatar URL for this profile.
        If the user has defined a custom URL, use it.
        If not, use Gravatar.
        :return: The avatar URL for this profile
        :rtype: str
        """
        if self.avatar_url:
            if self.avatar_url.startswith(settings.MEDIA_URL):
                return u"{}{}".format(settings.ZDS_APP["site"]["url"],
                                      self.avatar_url)
            else:
                return self.avatar_url
        else:
            return 'https://secure.gravatar.com/avatar/{0}?d=identicon'.format(
                md5(self.user.email.lower().encode("utf-8")).hexdigest())

    def set_avatar_from_file(self, avatar, filename='avatar.png'):
        """
        Updates the avatar of this user from a file, creating a gallery on his account
        if needed and adding the avatar to the gallery.
        :param avatar: The avatar file (file-like object).
        :param filename: The file name, including the type extension.
        """
        user_gallery = UserGallery.objects.filter(gallery__title=ZDS_APP['gallery']['avatars_gallery'], user=self.user)\
            .first()

        if not user_gallery:
            gallery = Gallery()
            gallery.title = ZDS_APP['gallery']['avatars_gallery']
            gallery.subtitle = ''
            gallery.slug = slugify(ZDS_APP['gallery']['avatars_gallery'])
            gallery.pubdate = datetime.now()
            gallery.save()

            user_gallery = UserGallery()
            user_gallery.gallery = gallery
            user_gallery.mode = GALLERY_WRITE
            user_gallery.user = self.user
            user_gallery.save()

        image = Image()
        image.title = _('Avatar')
        image.legend = _('Avatar importé')
        image.gallery = user_gallery.gallery
        image.physical = get_thumbnailer(avatar, relative_name=filename)
        image.pubdate = datetime.now()
        image.save()

        self.avatar_url = image.get_absolute_url()

    def get_title(self):
        if self.title:
            return self.title
        else:
            posts_count = self.get_post_count_as_staff()
            if posts_count > 10000:
                return "Dwarf Fortress 4 ever"
            elif posts_count > 9000:
                return "Over nine thousand!!!!"
            elif posts_count > 1501:
                return "Real Poney"
            elif posts_count > 1000:
                return "Little Poney"
            elif posts_count > 750:
                return "Représentant(e) en flood"
            elif posts_count > 600:
                return "Ender Dragon"
            elif posts_count > 500:
                return "N00B"
            elif posts_count > 400:
                return "Maraudeur rose"
            elif posts_count > 300:
                return "Vampire qui fait peur !"
            elif posts_count > 200:
                return "Toutou Noob"
            elif posts_count > 150:
                return "AK Carambar"
            elif posts_count > 100:
                return "Mouton laineux"
            elif posts_count > 60:
                return "Girafe"
            elif posts_count > 30:
                return "Koala"
            elif posts_count > 10:
                return "Petit chaton mignon"
            else:
                return "Petit nouveau"

    def get_post_count(self):
        """
        :return: The forum post count. Doesn't count comments on articles or tutorials.
        """
        return Post.objects.filter(author__pk=self.user.pk,
                                   is_visible=True).count()

    def get_post_count_as_staff(self):
        """Number of messages posted (view as staff)."""

        return Post.objects.filter(author__pk=self.user.pk).count()

    def get_topic_count(self):
        """
        :return: the number of topics created by this user.
        """
        return Topic.objects.filter(author=self.user).count()

    def get_user_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents with this user as author.
        """
        queryset = PublishableContent.objects.filter(authors__in=[self.user])

        if _type:
            queryset = queryset.filter(type=_type)

        return queryset

    def get_user_public_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents with this user as author.
        """
        queryset = PublishedContent.objects.filter(authors__in=[self.user])

        if _type:
            queryset = queryset.filter(content_type=_type)

        return queryset

    def get_content_count(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: the count of contents with this user as author. Count all contents no only published one.
        """
        if self.is_private():
            return 0
        return self.get_user_contents_queryset(_type).count()

    def get_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents with this user as author.
        """
        return self.get_user_contents_queryset(_type).all()

    def get_draft_contents(self, _type=None):
        """Return all draft contents with this user as author.
        A draft content is a content which is not published, in validation or in beta.

        :param _type: if provided, request a specific type of content
        :return: All draft tutorials with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_draft__isnull=False,
            sha_beta__isnull=True,
            sha_validation__isnull=True,
            sha_public__isnull=True).all()

    def get_public_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All published contents with this user as author.
        """
        return self.get_user_public_contents_queryset(_type).filter(
            must_redirect=False).all()

    def get_validate_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents in validation with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_validation__isnull=False).all()

    def get_beta_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All tutorials in beta with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_beta__isnull=False).all()

    def get_tuto_count(self):
        """
        :return: the count of tutorials with this user as author. Count all tutorials, no only published one.
        """
        return self.get_content_count(_type="TUTORIAL")

    def get_tutos(self):
        """
        :return: All tutorials with this user as author.
        """
        return self.get_contents(_type="TUTORIAL")

    def get_draft_tutos(self):
        """
        Return all draft tutorials with this user as author.
        A draft tutorial is a tutorial which is not published, in validation or in beta.
        :return: All draft tutorials with this user as author.
        """
        return self.get_draft_contents(_type="TUTORIAL")

    def get_public_tutos(self):
        """
        :return: All published tutorials with this user as author.
        """
        return self.get_public_contents(_type="TUTORIAL")

    def get_validate_tutos(self):
        """
        :return: All tutorials in validation with this user as author.
        """
        return self.get_validate_contents(_type="TUTORIAL")

    def get_beta_tutos(self):
        """
        :return: All tutorials in beta with this user as author.
        """
        return self.get_beta_contents(_type="TUTORIAL")

    def get_article_count(self):
        """
        :return: the count of articles with this user as author. Count all articles, no only published one.
        """
        return self.get_content_count(_type="ARTICLE")

    def get_articles(self):
        """
        :return: All articles with this user as author.
        """
        return self.get_contents(_type="ARTICLE")

    def get_public_articles(self):
        """
        :return: All published articles with this user as author.
        """
        return self.get_public_contents(_type="ARTICLE")

    def get_validate_articles(self):
        """
        :return: All articles in validation with this user as author.
        """
        return self.get_validate_contents(_type="ARTICLE")

    def get_draft_articles(self):
        """
        Return all draft article with this user as author.
        A draft article is a article which is not published or in validation.
        :return: All draft article with this user as author.
        """
        return self.get_draft_contents(_type="ARTICLE")

    def get_beta_articles(self):
        """
        :return: All articles in beta with this user as author.
        """
        return self.get_beta_contents(_type="ARTICLE")

    def get_posts(self):
        return Post.objects.filter(author=self.user).all()

    def get_invisible_posts_count(self):
        return Post.objects.filter(is_visible=False, author=self.user).count()

    # TODO: improve this method's name?
    def get_alerts_posts_count(self):
        """
        :return: The number of currently active alerts created by this user.
        """
        return Alert.objects.filter(author=self.user).count()

    def can_read_now(self):
        if self.user.is_authenticated:
            if self.user.is_active:
                if self.end_ban_read:
                    return self.can_read or (self.end_ban_read <
                                             datetime.now())
                else:
                    return self.can_read
            else:
                return False

    def can_write_now(self):
        if self.user.is_active:
            if self.end_ban_write:
                return self.can_write or (self.end_ban_write < datetime.now())
            else:
                return self.can_write
        else:
            return False

    def get_followed_topics(self):
        """
        :return: All forum topics followed by this user.
        """
        return Topic.objects.filter(topicfollowed__user=self.user)\
            .order_by('-last_message__pubdate')

    @staticmethod
    def has_read_permission(request):
        return True

    def has_object_read_permission(self, request):
        return True

    @staticmethod
    def has_write_permission(request):
        return True

    def has_object_write_permission(self, request):
        return self.has_object_update_permission(
            request) or request.user.has_perm("member.change_profile")

    def has_object_update_permission(self, request):
        return request.user.is_authenticated() and request.user == self.user

    @staticmethod
    def has_ban_permission(request):
        return True

    def has_object_ban_permission(self, request):
        return request.user and request.user.has_perm("member.change_profile")
コード例 #3
0
ファイル: models.py プロジェクト: pquentin/zds-site
class Profile(models.Model):
    """
    A user profile. Complementary data of standard Django `auth.user`.
    """
    class Meta:
        verbose_name = 'Profil'
        verbose_name_plural = 'Profils'
        permissions = (
            ("moderation", u"Modérer un membre"),
            ("show_ip", u"Afficher les IP d'un membre"),
        )

    # Link with standard user is a simple one-to-one link, as recommended in official documentation.
    # See https://docs.djangoproject.com/en/1.6/topics/auth/customizing/#extending-the-existing-user-model
    user = models.OneToOneField(User,
                                verbose_name='Utilisateur',
                                related_name="profile")

    last_ip_address = models.CharField('Adresse IP',
                                       max_length=39,
                                       blank=True,
                                       null=True)

    site = models.CharField('Site internet', max_length=2000, blank=True)
    show_email = models.BooleanField('Afficher adresse mail publiquement',
                                     default=False)

    avatar_url = models.CharField('URL de l\'avatar',
                                  max_length=2000,
                                  null=True,
                                  blank=True)

    biography = models.TextField('Biographie', blank=True)

    karma = models.IntegerField('Karma', default=0)

    sign = models.TextField('Signature', max_length=250, blank=True)

    show_sign = models.BooleanField('Voir les signatures', default=True)

    # TODO: Change this name. This is a boolean: "true" is "hover" or "click" ?!
    hover_or_click = models.BooleanField('Survol ou click ?', default=False)

    email_for_answer = models.BooleanField('Envoyer pour les réponse MP',
                                           default=False)

    # SdZ tutorial IDs separated by columns (:).
    # TODO: bad field name (singular --> should be plural), manually handled multi-valued field.
    sdz_tutorial = models.TextField('Identifiant des tutos SdZ',
                                    blank=True,
                                    null=True)

    can_read = models.BooleanField('Possibilité de lire', default=True)
    end_ban_read = models.DateTimeField('Fin d\'interdiction de lecture',
                                        null=True,
                                        blank=True)

    can_write = models.BooleanField('Possibilité d\'écrire', default=True)
    end_ban_write = models.DateTimeField('Fin d\'interdiction d\'ecrire',
                                         null=True,
                                         blank=True)

    last_visit = models.DateTimeField('Date de dernière visite',
                                      null=True,
                                      blank=True)

    objects = ProfileManager()
    _permissions = {}

    def __unicode__(self):
        return self.user.username

    def is_private(self):
        """checks the user can display his stats"""
        user_groups = self.user.groups.all()
        user_group_names = [g.name for g in user_groups]
        return settings.ZDS_APP['member']['bot_group'] in user_group_names

    def get_absolute_url(self):
        """Absolute URL to the profile page."""
        return reverse('member-detail',
                       kwargs={'user_name': self.user.username})

    def get_city(self):
        """
        Uses geo-localization to get physical localization of a profile through its last IP address.
        This works relatively good with IPv4 addresses (~city level), but is very imprecise with IPv6 or exotic internet
        providers.
        :return: The city and the country name of this profile.
        """
        # FIXME: this test to differentiate IPv4 and IPv6 addresses doesn't work, as IPv6 addresses may have length < 16
        # Example: localhost ("::1"). Real test: IPv4 addresses contains dots, IPv6 addresses contains columns.
        if len(self.last_ip_address) <= 16:
            gic = pygeoip.GeoIP(
                os.path.join(settings.GEOIP_PATH, 'GeoLiteCity.dat'))
        else:
            gic = pygeoip.GeoIP(
                os.path.join(settings.GEOIP_PATH, 'GeoLiteCityv6.dat'))

        geo = gic.record_by_addr(self.last_ip_address)

        if geo is not None:
            return u'{0}, {1}'.format(geo['city'], geo['country_name'])
        return ''

    def get_avatar_url(self):
        """Get the avatar URL for this profile.
        If the user has defined a custom URL, use it.
        If not, use Gravatar.
        :return: The avatar URL for this profile
        :rtype: str
        """
        if self.avatar_url:
            if self.avatar_url.startswith(settings.MEDIA_URL):
                return u"{}{}".format(settings.ZDS_APP["site"]["url"],
                                      self.avatar_url)
            else:
                return self.avatar_url
        else:
            return 'https://secure.gravatar.com/avatar/{0}?d=identicon'.format(
                md5(self.user.email.lower().encode("utf-8")).hexdigest())

    def get_post_count(self):
        """
        :return: The forum post count. Doesn't count comments on articles or tutorials.
        """
        return Post.objects.filter(author__pk=self.user.pk,
                                   is_visible=True).count()

    def get_post_count_as_staff(self):
        """Number of messages posted (view as staff)."""

        return Post.objects.filter(author__pk=self.user.pk).count()

    def get_topic_count(self):
        """
        :return: the number of topics created by this user.
        """
        return Topic.objects.filter(author=self.user).count()

    def get_user_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents with this user as author.
        """
        queryset = PublishableContent.objects.filter(authors__in=[self.user])

        if _type:
            queryset = queryset.filter(type=_type)

        return queryset

    def get_content_count(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: the count of contents with this user as author. Count all contents no only published one.
        """
        if self.is_private():
            return 0
        return self.get_user_contents_queryset(_type).count()

    def get_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents with this user as author.
        """
        return self.get_user_contents_queryset(_type).all()

    def get_draft_contents(self, _type=None):
        """Return all draft contents with this user as author.
        A draft content is a content which is not published, in validation or in beta.

        :param _type: if provided, request a specific type of content
        :return: All draft tutorials with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_draft__isnull=False,
            sha_beta__isnull=True,
            sha_validation__isnull=True,
            sha_public__isnull=True).all()

    def get_public_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All published contents with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_public__isnull=False).all()

    def get_validate_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents in validation with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_validation__isnull=False).all()

    def get_beta_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All tutorials in beta with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_beta__isnull=False).all()

    def get_tuto_count(self):
        """
        :return: the count of tutorials with this user as author. Count all tutorials, no only published one.
        """
        return self.get_content_count(_type="TUTORIAL")

    def get_tutos(self):
        """
        :return: All tutorials with this user as author.
        """
        return self.get_contents(_type="TUTORIAL")

    def get_draft_tutos(self):
        """
        Return all draft tutorials with this user as author.
        A draft tutorial is a tutorial which is not published, in validation or in beta.
        :return: All draft tutorials with this user as author.
        """
        return self.get_draft_contents(_type="TUTORIAL")

    def get_public_tutos(self):
        """
        :return: All published tutorials with this user as author.
        """
        return self.get_public_contents(_type="TUTORIAL")

    def get_validate_tutos(self):
        """
        :return: All tutorials in validation with this user as author.
        """
        return self.get_validate_contents(_type="TUTORIAL")

    def get_beta_tutos(self):
        """
        :return: All tutorials in beta with this user as author.
        """
        return self.get_beta_contents(_type="TUTORIAL")

    def get_article_count(self):
        """
        :return: the count of articles with this user as author. Count all articles, no only published one.
        """
        return self.get_content_count(_type="ARTICLE")

    def get_articles(self):
        """
        :return: All articles with this user as author.
        """
        return self.get_contents(_type="ARTICLE")

    def get_public_articles(self):
        """
        :return: All published articles with this user as author.
        """
        return self.get_public_contents(_type="ARTICLE")

    def get_validate_articles(self):
        """
        :return: All articles in validation with this user as author.
        """
        return self.get_validate_contents(_type="ARTICLE")

    def get_draft_articles(self):
        """
        Return all draft article with this user as author.
        A draft article is a article which is not published or in validation.
        :return: All draft article with this user as author.
        """
        return self.get_draft_contents(_type="ARTICLE")

    def get_beta_articles(self):
        """
        :return: All articles in beta with this user as author.
        """
        return self.get_beta_contents(_type="ARTICLE")

    def get_posts(self):
        return Post.objects.filter(author=self.user).all()

    def get_invisible_posts_count(self):
        return Post.objects.filter(is_visible=False, author=self.user).count()

    # TODO: improve this method's name?
    def get_alerts_posts_count(self):
        """
        :return: The number of currently active alerts created by this user.
        """
        return Alert.objects.filter(author=self.user).count()

    def can_read_now(self):
        if self.user.is_authenticated:
            if self.user.is_active:
                if self.end_ban_read:
                    return self.can_read or (self.end_ban_read <
                                             datetime.now())
                else:
                    return self.can_read
            else:
                return False

    def can_write_now(self):
        if self.user.is_active:
            if self.end_ban_write:
                return self.can_write or (self.end_ban_write < datetime.now())
            else:
                return self.can_write
        else:
            return False

    def get_followed_topics(self):
        """
        :return: All forum topics followed by this user.
        """
        return Topic.objects.filter(topicfollowed__user=self.user)\
            .order_by('-last_message__pubdate')
コード例 #4
0
ファイル: models.py プロジェクト: Arnaud-D/zds-site
class Profile(models.Model):
    """
    A user profile. Complementary data of standard Django `auth.user`.
    """
    class Meta:
        verbose_name = "Profil"
        verbose_name_plural = "Profils"
        permissions = (
            ("moderation", _("Modérer un membre")),
            ("show_ip", _("Afficher les IP d'un membre")),
        )

    # Link with standard user is a simple one-to-one link, as recommended in official documentation.
    # See https://docs.djangoproject.com/en/1.6/topics/auth/customizing/#extending-the-existing-user-model
    user = models.OneToOneField(User,
                                verbose_name="Utilisateur",
                                related_name="profile",
                                on_delete=models.CASCADE)
    username_skeleton = models.CharField("Squelette du username",
                                         max_length=150,
                                         null=True,
                                         blank=True,
                                         db_index=True)

    last_ip_address = models.CharField("Adresse IP",
                                       max_length=39,
                                       blank=True,
                                       null=True)

    site = models.CharField("Site internet", max_length=2000, blank=True)
    show_email = models.BooleanField("Afficher adresse mail publiquement",
                                     default=False)
    avatar_url = models.CharField("URL de l'avatar",
                                  max_length=2000,
                                  null=True,
                                  blank=True)
    biography = models.TextField("Biographie", blank=True)
    karma = models.IntegerField("Karma", default=0)
    sign = models.TextField("Signature", max_length=500, blank=True)
    licence = models.ForeignKey(Licence,
                                verbose_name="Licence préférée",
                                blank=True,
                                null=True,
                                on_delete=models.SET_NULL)
    github_token = models.TextField("GitHub", blank=True)
    show_sign = models.BooleanField("Voir les signatures", default=True)
    hide_forum_activity = models.BooleanField(
        "Masquer l'activité de forum et les commentaires sur la page de profil",
        default=False)
    # do UI components open by hovering them, or is clicking on them required?
    is_hover_enabled = models.BooleanField("Déroulement au survol ?",
                                           default=False)
    allow_temp_visual_changes = models.BooleanField(
        "Activer les changements visuels temporaires", default=True)
    show_markdown_help = models.BooleanField(
        "Afficher l'aide Markdown dans l'éditeur", default=True)
    email_for_answer = models.BooleanField("Envoyer pour les réponse MP",
                                           default=False)
    email_for_new_mp = models.BooleanField("Envoyer pour les nouveaux MP",
                                           default=False)
    hats = models.ManyToManyField(Hat,
                                  verbose_name="Casquettes",
                                  db_index=True,
                                  blank=True)
    can_read = models.BooleanField("Possibilité de lire", default=True)
    end_ban_read = models.DateTimeField("Fin d'interdiction de lecture",
                                        null=True,
                                        blank=True)
    can_write = models.BooleanField("Possibilité d'écrire", default=True)
    end_ban_write = models.DateTimeField("Fin d'interdiction d'écrire",
                                         null=True,
                                         blank=True)
    last_visit = models.DateTimeField("Date de dernière visite",
                                      null=True,
                                      blank=True)
    _permissions = {}
    _groups = None
    _cached_city = None

    objects = ProfileManager()

    def __str__(self):
        return self.user.username

    def is_private(self):
        """Can the user display their stats?"""
        user_groups = self.user.groups.all()
        user_group_names = [g.name for g in user_groups]
        return settings.ZDS_APP["member"]["bot_group"] in user_group_names

    def get_absolute_url(self):
        """Absolute URL to the profile page."""
        return reverse("member-detail",
                       kwargs={"user_name": self.user.username})

    def get_city(self):
        """
        Uses geo-localization to get physical localization of a profile through its last IP address.
        This works relatively well with IPv4 addresses (~city level), but is very imprecise with IPv6 or exotic internet
        providers.
        The result is cached on an instance level because this method is called a lot in the profile.
        :return: The city and the country name of this profile.
        """
        if self._cached_city is not None and self._cached_city[
                0] == self.last_ip_address:
            return self._cached_city[1]

        try:
            geo = GeoIP2().city(self.last_ip_address)
        except AddressNotFoundError:
            self._cached_city = (self.last_ip_address, "")
            return ""

        city = geo["city"]
        country = geo["country_name"]
        geo_location = ", ".join(i for i in [city, country] if i)

        self._cached_city = (self.last_ip_address, geo_location)
        return geo_location

    def get_avatar_url(self):
        """Get the avatar URL for this profile.
        If the user has defined a custom URL, use it.
        If not, use Gravatar.
        :return: The avatar URL for this profile
        :rtype: str
        """
        if self.avatar_url:
            if self.avatar_url.startswith(settings.MEDIA_URL):
                return "{}{}".format(settings.ZDS_APP["site"]["url"],
                                     self.avatar_url)
            else:
                return self.avatar_url
        else:
            return "https://secure.gravatar.com/avatar/{}?d=identicon".format(
                md5(self.user.email.lower().encode("utf-8")).hexdigest())

    def get_post_count(self):
        """
        :return: The forum post count. Doesn't count comments on articles or tutorials.
        """
        return Post.objects.filter(author__pk=self.user.pk,
                                   is_visible=True).count()

    def get_post_count_as_staff(self):
        """Number of messages posted (view as staff)."""

        return Post.objects.filter(author__pk=self.user.pk).count()

    def get_topic_count(self):
        """
        :return: the number of topics created by this user.
        """
        return Topic.objects.filter(author=self.user).count()

    def get_user_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents with this user as author.
        """
        queryset = PublishableContent.objects.filter(authors__in=[self.user])

        if _type:
            queryset = queryset.filter(type=_type)

        return queryset

    def get_user_public_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of public contents with this user as author.
        """
        queryset = PublishableContent.objects.filter(
            public_version__authors__in=[self.user])

        if _type:
            queryset = queryset.filter(type=_type)

        return queryset

    def get_user_draft_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of draft contents with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_draft__isnull=False,
            sha_beta__isnull=True,
            sha_validation__isnull=True,
            sha_public__isnull=True)

    def get_user_validate_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents in validation with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_validation__isnull=False)

    def get_user_beta_contents_queryset(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: Queryset of contents in beta with this user as author.
        """
        return self.get_user_contents_queryset(_type).filter(
            sha_beta__isnull=False)

    def get_content_count(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: the count of contents with this user as author. Count all contents no only published one.
        """
        if self.is_private():
            return 0
        return self.get_user_contents_queryset(_type).count()

    def get_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents with this user as author.
        """
        return self.get_user_contents_queryset(_type).all()

    def get_draft_contents(self, _type=None):
        """Return all draft contents with this user as author.
        A draft content is a content which is not published, in validation or in beta.

        :param _type: if provided, request a specific type of content
        :return: All draft tutorials with this user as author.
        """
        return self.get_user_draft_contents_queryset(_type).all()

    def get_public_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All published contents with this user as author.
        """
        return self.get_user_public_contents_queryset(_type).all()

    def get_validate_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All contents in validation with this user as author.
        """
        return self.get_user_validate_contents_queryset(_type).all()

    def get_beta_contents(self, _type=None):
        """
        :param _type: if provided, request a specific type of content
        :return: All tutorials in beta with this user as author.
        """
        return self.get_user_beta_contents_queryset(_type).all()

    def get_tuto_count(self):
        """
        :return: the count of tutorials with this user as author. Count all tutorials, no only published one.
        """
        return self.get_content_count(_type="TUTORIAL")

    def get_tutos(self):
        """
        :return: All tutorials with this user as author.
        """
        return self.get_contents(_type="TUTORIAL")

    def get_draft_tutos(self):
        """
        Return all draft tutorials with this user as author.
        A draft tutorial is a tutorial which is not published, in validation or in beta.
        :return: All draft tutorials with this user as author.
        """
        return self.get_draft_contents(_type="TUTORIAL")

    def get_public_tutos(self):
        """
        :return: All published tutorials with this user as author.
        """
        return self.get_public_contents(_type="TUTORIAL")

    def get_validate_tutos(self):
        """
        :return: All tutorials in validation with this user as author.
        """
        return self.get_validate_contents(_type="TUTORIAL")

    def get_beta_tutos(self):
        """
        :return: All tutorials in beta with this user as author.
        """
        return self.get_beta_contents(_type="TUTORIAL")

    def get_article_count(self):
        """
        :return: the count of articles with this user as author. Count all articles, no only published one.
        """
        return self.get_content_count(_type="ARTICLE")

    def get_articles(self):
        """
        :return: All articles with this user as author.
        """
        return self.get_contents(_type="ARTICLE")

    def get_public_articles(self):
        """
        :return: All published articles with this user as author.
        """
        return self.get_public_contents(_type="ARTICLE")

    def get_validate_articles(self):
        """
        :return: All articles in validation with this user as author.
        """
        return self.get_validate_contents(_type="ARTICLE")

    def get_draft_articles(self):
        """
        Return all draft article with this user as author.
        A draft article is a article which is not published or in validation.
        :return: All draft article with this user as author.
        """
        return self.get_draft_contents(_type="ARTICLE")

    def get_beta_articles(self):
        """
        :return: All articles in beta with this user as author.
        """
        return self.get_beta_contents(_type="ARTICLE")

    def get_opinion_count(self):
        """
        :return: the count of opinions with this user as author. Count all opinions, no only published one.
        """
        return self.get_content_count(_type="OPINION")

    def get_opinions(self):
        """
        :return: All opinions with this user as author.
        """
        return self.get_contents(_type="OPINION")

    def get_public_opinions(self):
        """
        :return: All published opinions with this user as author.
        """
        return self.get_public_contents(_type="OPINION")

    def get_draft_opinions(self):
        """
        Return all draft opinion with this user as author.
        A draft opinion is a opinion which is not published or in validation.
        :return: All draft opinion with this user as author.
        """
        return self.get_draft_contents(_type="OPINION")

    def get_posts(self):
        return Post.objects.filter(author=self.user).all()

    def get_hidden_by_staff_posts_count(self):
        return Post.objects.filter(
            is_visible=False,
            author=self.user).exclude(editor=self.user).count()

    def get_active_alerts_count(self):
        """
        :return: The number of currently active alerts created by this user.
        """
        return Alert.objects.filter(author=self.user, solved=False).count()

    def can_read_now(self):
        if self.user.is_authenticated:
            if self.user.is_active:
                if self.end_ban_read:
                    return self.can_read or (self.end_ban_read <
                                             datetime.now())
                return self.can_read
            return False

    def can_write_now(self):
        if self.user.is_active:
            if self.end_ban_write:
                return self.can_write or (self.end_ban_write < datetime.now())
            return self.can_write
        return False

    def get_followed_topics(self):
        """
        :return: All forum topics followed by this user.
        """
        return TopicAnswerSubscription.objects.get_objects_followed_by(
            self.user.id)

    def get_followed_topic_count(self):
        """
        :return: the number of topics followeded by this user.
        """
        return TopicAnswerSubscription.objects.get_objects_followed_by(
            self.user.id).count()

    def is_dev(self):
        """
        Checks whether user is part of group `settings.ZDS_APP['member']['dev_group']`.
        """
        return self.user.groups.filter(
            name=settings.ZDS_APP["member"]["dev_group"]).exists()

    def has_hat(self):
        """
        Checks if this user can at least use one hat.
        """
        return len(self.get_hats()) >= 1

    def get_hats(self):
        """
        Return all hats the user is allowed to use.
        """
        profile_hats = list(self.hats.all())
        groups_hats = list(
            Hat.objects.filter(group__in=self.user.groups.all()))
        hats = profile_hats + groups_hats

        # We sort internal hats before the others, and we slugify for sorting to sort correctly
        # with diatrics.
        hats.sort(key=lambda hat:
                  f'{"a" if hat.is_staff else "b"}-{old_slugify(hat.name)}')

        return hats

    def get_requested_hats(self):
        """
        Return all current hats requested by this user.
        """
        return self.user.requested_hats.filter(
            is_granted__isnull=True).order_by("-date")

    def get_solved_hat_requests(self):
        """
        Return old hats requested by this user.
        """
        return self.user.requested_hats.filter(
            is_granted__isnull=False).order_by("-solved_at")

    @staticmethod
    def has_read_permission(request):
        return True

    def has_object_read_permission(self, request):
        return True

    @staticmethod
    def has_write_permission(request):
        return True

    def has_object_write_permission(self, request):
        return self.has_object_update_permission(
            request) or request.user.has_perm("member.change_profile")

    def has_object_update_permission(self, request):
        return request.user.is_authenticated and request.user == self.user

    @staticmethod
    def has_ban_permission(request):
        return True

    def has_object_ban_permission(self, request):
        return request.user and request.user.has_perm("member.change_profile")

    @property
    def group_pks(self):
        if self._groups is None:
            self._groups = list(self.user.groups.all())
        return [g.pk for g in self._groups]

    @staticmethod
    def find_username_skeleton(username):
        skeleton = ""
        for ch in username:
            homoglyph = hg.Homoglyphs(languages={"fr"},
                                      strategy=hg.STRATEGY_LOAD).to_ascii(ch)
            if len(homoglyph) > 0:
                if homoglyph[0].strip() != "":
                    skeleton += homoglyph[0]
        return skeleton.lower()