Changeset View
Changeset View
Standalone View
Standalone View
profiles/models.py
| from typing import Optional | from typing import Optional | ||||
| import logging | import logging | ||||
| import botocore | |||||
| import requests | |||||
| from actstream.models import Action | from actstream.models import Action | ||||
| from django import urls | from django import urls | ||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||
| from django.db import models | from django.db import models | ||||
| from django.db.models import Case, When, Value, IntegerField | from django.db.models import Case, When, Value, IntegerField | ||||
| from django.templatetags.static import static | |||||
| from django.urls import reverse | from django.urls import reverse | ||||
| from common import mixins | from common import mixins | ||||
| from common.upload_paths import get_upload_to_hashed_path | |||||
| from profiles.blender_id import BIDSession | from profiles.blender_id import BIDSession | ||||
| bid = BIDSession() | bid = BIDSession() | ||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| class Profile(mixins.CreatedUpdatedMixin, models.Model): | class Profile(mixins.CreatedUpdatedMixin, models.Model): | ||||
| """Store additional Profile data, such as avatar and full name.""" | """Store additional Profile data, such as image and full name.""" | ||||
| class Meta: | class Meta: | ||||
| permissions = [('can_view_content', 'Can view subscription-only content')] | permissions = [('can_view_content', 'Can view subscription-only content')] | ||||
| user = models.OneToOneField( | user = models.OneToOneField( | ||||
| User, primary_key=True, on_delete=models.CASCADE, related_name='profile' | User, primary_key=True, on_delete=models.CASCADE, related_name='profile' | ||||
| ) | ) | ||||
| full_name = models.CharField(max_length=255, blank=True, default='') | full_name = models.CharField(max_length=255, blank=True, default='') | ||||
| image = models.ImageField(upload_to=get_upload_to_hashed_path, blank=True) | |||||
| is_subscribed_to_newsletter = models.BooleanField(default=False) | is_subscribed_to_newsletter = models.BooleanField(default=False) | ||||
| def get_absolute_url(self): | def get_absolute_url(self): | ||||
| """Return absolute URL of a Profile.""" | """Return absolute URL of a Profile.""" | ||||
| return urls.reverse('profile_detail', kwargs={'pk': self.pk}) | return urls.reverse('profile_detail', kwargs={'pk': self.pk}) | ||||
| def __str__(self) -> str: | def __str__(self) -> str: | ||||
| return f'Profile {self.user}' | return f'Profile {self.user}' | ||||
| @property | @property | ||||
| def image_url(self) -> Optional[str]: | def image_url(self) -> Optional[str]: | ||||
| """Return a URL of the Profile image.""" | """Return a URL of the Profile image.""" | ||||
| if not getattr(self.user, 'oauth_info', None) or not getattr( | if not self.image: | ||||
| self.user.oauth_info, 'oauth_user_id' | return static('common/images/blank-profile-pic.png') | ||||
| ): | |||||
| return None | |||||
| oauth_info = self.user.oauth_info | return self.image.url | ||||
| return bid.get_avatar_url(oauth_info.oauth_user_id) | |||||
| @property | @property | ||||
| def notifications(self): | def notifications(self): | ||||
| return ( | return ( | ||||
| self.user.notifications.select_related('action').annotate( | self.user.notifications.select_related('action').annotate( | ||||
| unread=Case( | unread=Case( | ||||
| When(date_read__isnull=True, then=Value(0)), | When(date_read__isnull=True, then=Value(0)), | ||||
| default=Value(1), | default=Value(1), | ||||
| output_field=IntegerField(), | output_field=IntegerField(), | ||||
| ) | ) | ||||
| ) | ) | ||||
| # Unread notifications come first | # Unread notifications come first | ||||
| .order_by('unread', '-date_created') | .order_by('unread', '-date_created') | ||||
| ) | ) | ||||
| @property | @property | ||||
| def notifications_unread(self): | def notifications_unread(self): | ||||
| return self.notifications.filter(date_read__isnull=True) | return self.notifications.filter(date_read__isnull=True) | ||||
| def copy_avatar_from_blender_id(self): | |||||
| """ | |||||
| Attempt to retrieve an avatar from Blender ID and save it into our storage. | |||||
| If either OAuth info or Blender ID service isn't available, log an error and return. | |||||
| """ | |||||
| if not hasattr(self.user, 'oauth_info'): | |||||
| logger.warning(f'Cannot copy avatar from Blender ID: {self} is missing OAuth info') | |||||
| return | |||||
| oauth_info = self.user.oauth_info | |||||
| try: | |||||
| name, content = bid.get_avatar(oauth_info.oauth_user_id) | |||||
| if self.image: | |||||
| # Delete the previous file | |||||
| self.image.delete(save=False) | |||||
| self.image.save(name, content, save=True) | |||||
| logger.info(f'Profile image updated for {self}') | |||||
| except requests.HTTPError: | |||||
| logger.exception(f'Failed to retrieve an image for {self} from Blender ID') | |||||
| except botocore.exceptions.BotoCoreError: | |||||
| logger.exception(f'Failed to store an image for {self}') | |||||
| except Exception: | |||||
| logger.exception(f'Failed to copy an image for {self}') | |||||
| class Notification(models.Model): | class Notification(models.Model): | ||||
| """Store additional data about an actstream notification. | """Store additional data about an actstream notification. | ||||
| In general, it's not easy to determine if an action qualifies as a notification | In general, it's not easy to determine if an action qualifies as a notification | ||||
| for a certain user because of the variaty of targets | for a certain user because of the variaty of targets | ||||
| (assets, comments with relations to different pages and so on), | (assets, comments with relations to different pages and so on), | ||||
| so it's best to link actions to their relevant users when a new action is created. | so it's best to link actions to their relevant users when a new action is created. | ||||
| Show All 16 Lines | |||||