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]
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")
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')
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()