class AbstractProposal(EventInfo): submitter = BigForeignKey( to=settings.AUTH_USER_MODEL, verbose_name=_('submitter'), ) objective = EAWTextField( verbose_name=_('objective'), max_length=1000, help_text=_( "Who is the intended audience for your talk? (Be specific, " "\"Python users\" is not a good answer). " "And what will the attendees get out of your talk? When they " "leave the room, what will they learn that they didn't know " "before? This is NOT made public and for REVIEW ONLY." ), ) supplementary = models.TextField( verbose_name=_('supplementary'), blank=True, default='', help_text=_( "Anything else you'd like the program committee to know when " "making their selection: your past speaking experience, community " "experience, etc. This is NOT made public and for REVIEW ONLY. " "Edit using " "<a href='http://daringfireball.net/projects/markdown/basics' " "target='_blank'>Markdown</a>." ), ) cancelled = models.BooleanField( verbose_name=_('cancelled'), default=False, db_index=True, ) additionalspeaker_set = GenericRelation( to=AdditionalSpeaker, content_type_field='proposal_type', object_id_field='proposal_id', ) objects = ProposalQuerySet.as_manager() class Meta: abstract = True @property def speakers(self): yield PrimarySpeaker(self) additionals = self.additionalspeaker_set.filter(cancelled=False) for speaker in additionals.select_related('user'): yield speaker @property def speaker_count(self): return self.additionalspeaker_set.filter(cancelled=False).count() + 1
class AbstractProposal(ConferenceRelated, EventInfo): submitter = BigForeignKey( to=settings.AUTH_USER_MODEL, verbose_name=_('submitter'), on_delete=models.CASCADE, ) outline = models.TextField( verbose_name=_('outline'), blank=True, ) objective = EAWTextField( verbose_name=_('objective'), max_length=1000, ) supplementary = models.TextField( verbose_name=_('supplementary'), blank=True, default='', ) cancelled = models.BooleanField( verbose_name=_('cancelled'), default=False, db_index=True, ) ACCEPTED_CHOICES = ( (None, '----------'), (True, _('Accepted')), (False, _('Rejected')), ) accepted = models.NullBooleanField( verbose_name=_('accepted'), default=None, choices=ACCEPTED_CHOICES, db_index=True, ) additionalspeaker_set = GenericRelation( to=AdditionalSpeaker, content_type_field='proposal_type', object_id_field='proposal_id', ) objects = DefaultConferenceManager.from_queryset(ProposalQuerySet)() all_objects = ProposalQuerySet.as_manager() _must_fill_fields = [ 'abstract', 'objective', 'supplementary', 'detailed_description', 'outline', ] class Meta: abstract = True @property def speakers(self): yield PrimarySpeaker(proposal=self) # Optimization: Callers of this method can annotate the queryset to # avoid lookups when a proposal doesn't have any additional speakers. with contextlib.suppress(AttributeError): if self._additional_speaker_count < 1: return # Optimization: Callers of this method can prefetch the additional # speaker queryset to avoid n+1 lookups when operating on multiple # proposals. Example:: # # proposals = TalkProposal.objects.prefetch_related(Prefetch( # 'additionalspeaker_set', # to_attr='_additional_speakers', # queryset=( # AdditionalSpeaker.objects # .filter(cancelled=False) # .select_related('user') # ), # )) # for p in proposals: # Only two queries: proposals, and speakers. # for s in p.speakers: # print(s.user.email) try: additionals = self._additional_speakers except AttributeError: additionals = ( self.additionalspeaker_set .filter(cancelled=False) .select_related('user') ) for speaker in additionals: yield speaker @property def speaker_count(self): # Optimization: Callers of this method can annotate the queryset to # avoid n+1 lookups when operating on multiple proposals. try: count = self._additional_speaker_count except AttributeError: count = self.additionalspeaker_set.filter(cancelled=False).count() return count + 1 @property def must_fill_fields_count(self): return len(self._must_fill_fields) @property def finished_fields_count(self): count = sum(1 for f in self._must_fill_fields if getattr(self, f)) return count @property def finish_percentage(self): return 100 * self.finished_fields_count // self.must_fill_fields_count @property def unfinished_fields_count(self): return self.must_fill_fields_count - self.finished_fields_count
class User(AbstractBaseUser, PermissionsMixin): email = models.EmailField( verbose_name=_('email address'), max_length=255, unique=True, db_index=True, ) speaker_name = models.CharField( verbose_name=_('speaker name'), max_length=100, ) bio = EAWTextField( verbose_name=_('biography'), max_length=1000, help_text=_( "Describe yourself with 1000 characters or less. " "There will be no formatting." ), ) photo = models.ImageField( verbose_name=_('photo'), blank=True, default='', upload_to=photo_upload_to, ) facebook_profile_url = models.URLField( verbose_name=_('Facebook'), blank=True, help_text=format_html_lazy(_( "Link to your Facebook profile page. This will be shown when " "we display your public information. If you do not know what your " "profile page link is, click on " "<a href='https://www.facebook.com/me' target='_blank'>" "this link</a>, and copy-paste the URL of the page opened. " "Remember to log in to Facebook first!" )), ) twitter_id = models.CharField( verbose_name=_('Twitter'), blank=True, max_length=100, validators=[ RegexValidator(r'^[0-9a-zA-Z_]*$', 'Not a valid Twitter handle'), ], help_text=_( "Your Twitter handle, without the \"@\" sign. This will be " "shown when we display your public information." ), ) github_id = models.CharField( verbose_name=_('GitHub'), blank=True, max_length=100, validators=[ RegexValidator(r'^[0-9a-zA-Z_-]*$', 'Not a valid GitHub account'), ], help_text=_( "Your GitHub account, without the \"@\" sign. This will be " "shown when we display your public information." ), ) verified = models.BooleanField( verbose_name=_('verified'), default=False, help_text=_( "Designates whether the user has verified email ownership." ), ) is_staff = models.BooleanField( verbose_name=_('staff status'), default=False, help_text=_( "Designates whether the user can log into this admin site." ), ) is_active = models.BooleanField( verbose_name=_('active'), default=True, help_text=_( "Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts." ), ) date_joined = models.DateTimeField( verbose_name=_('date joined'), default=timezone.now, ) objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] class Meta: verbose_name = _('user') verbose_name_plural = _('users') swappable = 'AUTH_USER_MODEL' def __str__(self): return self.email def as_hash(self): """Return the user's hash representation. Pipeline: - pk -> md5 -> first 2 bytes -> b16 -> str. """ return '#{hash_user}'.format( hash_user=( base64.b16encode( # Picking the first 2 bytes may still run into hash # collisions if you take the whole users list into account, # but as this is used for representing reviewers which is a # comparatively small subset, it's not a big deal. hashlib.md5(str(self.pk).encode('utf-8')).digest()[:2], ).decode('utf-8') ) ) as_hash.short_description = _('Reviewer ID') def get_full_name(self): return self.speaker_name def get_short_name(self): return self.speaker_name def get_thumbnail_url(self): """Return URL to compressed profile photo if set, or a default image. """ if self.photo and os.path.exists(self.photo.path): return get_thumbnail(self.photo, '800x800').url return static('images/default_head.png') def is_valid_speaker(self): """Whether the user is a valid speaker. :seealso: ``UserQuerySet.get_valid_speakers`` """ return ( self.verified and self.is_active and self.speaker_name and self.bio ) def is_reviewer(self): return self.has_perm('reviews.add_review') @property def cospeaking_info_set(self): return self.additionalspeaker_set.filter( cancelled=False, conference=settings.CONFERENCE_DEFAULT_SLUG, ) @property def twitter_profile_url(self): if not self.twitter_id: return "" return 'https://twitter.com/{}'.format(self.twitter_id) @property def github_profile_url(self): if not self.github_id: return "" return 'https://github.com/{}'.format(self.github_id) def get_verification_key(self): key = signing.dumps( obj=getattr(self, self.USERNAME_FIELD), salt=settings.SECRET_KEY, ) return key def email_user(self, subject, message, from_email=None, **kwargs): """Send an email to this user. """ send_mail(subject, message, from_email, [self.email], **kwargs) def send_verification_email(self, request): verification_key = self.get_verification_key() verification_url = request.build_absolute_uri( reverse('user_verify', kwargs={ 'verification_key': verification_key, }), ) context = { 'user': self, 'host': request.get_host(), 'verification_key': verification_key, 'verification_url': verification_url, } message = render_to_string( 'registration/verification_email.txt', context, ) self.email_user( subject=gettext('Verify your email address on {host}').format( **context ), message=message, fail_silently=False, ) @property def has_agreed_coc(self): return CocRecord.objects.filter(user=self, coc_version=settings.COC_VERSION).count() == 1
class AbstractProposal(ConferenceRelated, EventInfo): submitter = BigForeignKey( to=settings.AUTH_USER_MODEL, verbose_name=_('submitter'), ) outline = models.TextField( verbose_name=_('outline'), blank=True, ) objective = EAWTextField( verbose_name=_('objective'), max_length=1000, ) supplementary = models.TextField( verbose_name=_('supplementary'), blank=True, default='', ) cancelled = models.BooleanField( verbose_name=_('cancelled'), default=False, db_index=True, ) additionalspeaker_set = GenericRelation( to=AdditionalSpeaker, content_type_field='proposal_type', object_id_field='proposal_id', ) objects = DefaultConferenceManager.from_queryset(ProposalQuerySet)() all_objects = ProposalQuerySet.as_manager() _must_fill_fields = [ 'abstract', 'objective', 'supplementary', 'detailed_description', 'outline', ] class Meta: abstract = True @property def speakers(self): yield PrimarySpeaker(proposal=self) additionals = self.additionalspeaker_set.filter(cancelled=False) for speaker in additionals.select_related('user'): yield speaker @property def speaker_count(self): return self.additionalspeaker_set.filter(cancelled=False).count() + 1 @property def must_fill_fields_count(self): return len(self._must_fill_fields) @property def finished_fields_count(self): count = sum(1 for f in self._must_fill_fields if getattr(self, f)) return count @property def finish_percentage(self): return 100 * self.finished_fields_count // self.must_fill_fields_count @property def unfinished_fields_count(self): return self.must_fill_fields_count - self.finished_fields_count
class User(AbstractBaseUser, PermissionsMixin): email = models.EmailField( verbose_name=_('email address'), max_length=255, unique=True, db_index=True, ) speaker_name = models.CharField( verbose_name=_('speaker name'), max_length=100, ) bio = EAWTextField( verbose_name=_('biography'), max_length=1000, help_text=_( "Describe yourself with 500 characters or less. " "There will be no formatting." ), ) photo = models.ImageField( verbose_name=_('photo'), blank=True, default='', upload_to=photo_upload_to, ) facebook_profile_url = models.URLField( verbose_name=_('Facebook'), blank=True, help_text=format_html_lazy(_( "Link to your Facebook profile page. This will be shown when " "we display your public information. If you do not know what your " "profile page link is, click on " "<a href='https://www.facebook.com/me' target='_blank'>" "this link</a>, and copy-paste the URL of the page opened. " "Remember to log in to Facebook first!" )), ) twitter_id = models.CharField( verbose_name=_('Twitter'), blank=True, max_length=100, validators=[ RegexValidator(r'^[0-9a-zA-Z_]*$', 'Not a valid Twitter handle'), ], help_text=_( "Your Twitter handle, without the \"@\" sign. This will be " "shown when we display your public information." ), ) github_id = models.CharField( verbose_name=_('GitHub'), blank=True, max_length=100, validators=[ RegexValidator(r'^[0-9a-zA-Z_-]*$', 'Not a valid GitHub account'), ], help_text=_( "Your GitHub account, without the \"@\" sign. This will be " "shown when we display your public information." ), ) verified = models.BooleanField( verbose_name=_('verified'), default=False, help_text=_( "Designates whether the user has verified email ownership." ), ) is_staff = models.BooleanField( verbose_name=_('staff status'), default=False, help_text=_( "Designates whether the user can log into this admin site." ), ) is_active = models.BooleanField( verbose_name=_('active'), default=True, help_text=_( "Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts." ), ) date_joined = models.DateTimeField( verbose_name=_('date joined'), default=timezone.now, ) objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] class Meta: verbose_name = _('user') verbose_name_plural = _('users') swappable = 'AUTH_USER_MODEL' def __str__(self): return self.email def get_full_name(self): return self.speaker_name def get_short_name(self): return self.speaker_name def is_valid_speaker(self): """Whether the user is a valid speaker. :seealso: ``UserQuerySet.get_valid_speakers`` """ return ( self.verified and self.is_active and self.speaker_name and self.bio ) def is_reviewer(self): return self.has_perm('reviews.add_review') @property def cospeaking_info_set(self): return self.additionalspeaker_set.filter( cancelled=False, conference=settings.CONFERENCE_DEFAULT_SLUG, ) @property def twitter_profile_url(self): return 'https://twitter.com/{}'.format(self.twitter_id) @property def github_profile_url(self): return 'https://github.com/{}'.format(self.github_id) def get_verification_key(self): key = signing.dumps( obj=getattr(self, self.USERNAME_FIELD), salt=settings.SECRET_KEY, ) return key def email_user(self, subject, message, from_email=None, **kwargs): """Send an email to this user. """ send_mail(subject, message, from_email, [self.email], **kwargs) def send_verification_email(self, request): verification_key = self.get_verification_key() verification_url = request.build_absolute_uri( reverse('user_verify', kwargs={ 'verification_key': verification_key, }), ) context = { 'user': self, 'host': request.get_host(), 'verification_key': verification_key, 'verification_url': verification_url, } message = render_to_string( 'registration/verification_email.txt', context, ) self.email_user( subject=ugettext('Verify your email address on {host}').format( **context ), message=message, fail_silently=False, )