def send_to_teams(received_message, package_name, keyword): """ Sends the given email message to all members of each team that has the given package. The message is only sent to those users who have not muted the team and have the given keyword in teir set of keywords for the team membership. :param received_message: The modified received package message to be sent to the subscribers. :type received_message: :py:class:`email.message.Message` or an equivalent interface object :param package_name: The name of the package for which this message was intended. :type package_name: string :param keyword: The keyword with which the message should be tagged :type keyword: string """ keyword = get_or_none(Keyword, name=keyword) package = get_or_none(PackageName, name=package_name) if not keyword or not package: return # Get all teams that have the given package teams = Team.objects.filter(packages=package) teams = teams.prefetch_related('team_membership_set') for team in teams: send_to_team(received_message, team, keyword, package.name)
def pre_confirm(self): """ Implementation of a hook method which is executed instead of :py:meth:`handle` when the command is not confirmed. """ if not PackageName.objects.exclude(binary=True).filter(name=self.package).exists(): if BinaryPackageName.objects.exists_with_name(self.package): binary_package = BinaryPackageName.objects.get_by_name(self.package) self.warn('{package} is not a source package.'.format( package=self.package)) self.reply('{package} is the source package ' 'for the {binary} binary package'.format( package=binary_package.main_source_package_name, binary=binary_package.name)) self.package = binary_package.main_source_package_name.name else: self.warn( '{package} is neither a source package ' 'nor a binary package.'.format(package=self.package)) return False user = get_or_none(UserEmail, email=self.user_email) emailsettings = get_or_none(EmailSettings, user_email=user) if not user or not emailsettings.is_subscribed_to(self.package): self.error( "{email} is not subscribed, you can't unsubscribe.".format( email=self.user_email) ) return False self.reply('A confirmation mail has been sent to ' + self.user_email) return True
def get_subscription(self, email, package_name): """ Helper method returning a :py:class:`Subscription <distro_tracker.core.models.Subscription>` instance for the given package and user. It logs any errors found while retrieving this instance, such as the user not being subscribed to the given package. :param email: The email of the user. :param package_name: The name of the package. """ user_email = get_or_none(UserEmail, email=email) email_settings = get_or_none(EmailSettings, user_email=user_email) if not user_email or not email_settings: self.error_not_subscribed(email, package_name) return package = get_or_none(PackageName, name=package_name) if not package: self.error('Package {package} does not exist'.format( package=package_name)) return subscription = get_or_none(Subscription, package=package, email_settings=email_settings) if not subscription: self.error_not_subscribed(email, package_name) return subscription
def send_to_teams(received_message, package_name, keyword): """ Sends the given email message to all members of each team that has the given package. The message is only sent to those users who have not muted the team and have the given keyword in teir set of keywords for the team membership. :param received_message: The modified received package message to be sent to the subscribers. :type received_message: :py:class:`email.message.Message` or an equivalent interface object :param package_name: The name of the package for which this message was intended. :type package_name: string :param keyword: The keyword with which the message should be tagged :type keyword: string """ keyword = get_or_none(Keyword, name=keyword) package = get_or_none(PackageName, name=package_name) if not keyword or not package: return # Get all teams that have the given package teams = Team.objects.filter(packages=package) teams = teams.prefetch_related('team_membership_set') date = timezone.now().date() messages_to_send = [] for team in teams: logger.info('dispatch :: sending to team %s', team.slug) team_message = deepcopy(received_message) add_team_membership_headers( team_message, package_name, keyword.name, team) # Send the message to each member of the team for membership in team.team_membership_set.all(): # Do not send messages to muted memberships if membership.is_muted(package): continue # Do not send the message if the user has disabled the keyword if keyword not in membership.get_keywords(package): continue messages_to_send.append(prepare_message( team_message, membership.user_email.email, date)) send_messages(messages_to_send, date)
def pre_confirm(self): """ Implementation of a hook method which is executed instead of :py:meth:`handle` when the command is not confirmed. """ user = get_or_none(UserEmail, email=self.user_email) email_settings = get_or_none(EmailSettings, user_email=user) if not user or not email_settings or email_settings.subscription_set.count() == 0: self.warn('User {email} is not subscribed to any packages'.format( email=self.user_email)) return False self.reply('A confirmation mail has been sent to {email}'.format( email=self.user_email)) return True
def add_keyword_to_subscriptions(self, new_keyword, existing_keyword): """ Adds the given ``new_keyword`` to each :py:class:`Subscription <distro_tracker.core.models.Subscription>`'s keywords list which already contains the ``existing_keyword``. :param new_keyword: The keyword to add to the :py:class:`Subscription <distro_tracker.core.models.Subscription>`'s keywords :type new_keyword: :py:class:`Keyword <distro_tracker.core.models.Keyword>` :param existing_keyword: The keyword or name of the keyword based on which all :py:class:`Subscription <distro_tracker.core.models.Subscription>` to which the ``new_keyword`` should be added are chosen. :type existing_keyword: :py:class:`Keyword <distro_tracker.core.models.Keyword>` or string """ if not isinstance(existing_keyword, Keyword): existing_keyword = get_or_none(Keyword, name=existing_keyword) if not existing_keyword: raise CommandError("Given keyword does not exist. No actions taken.") self.add_keyword_to_user_defaults( new_keyword, EmailSettings.objects.filter(default_keywords=existing_keyword) ) for subscription in Subscription.objects.all(): if existing_keyword in subscription.keywords.all(): if subscription._use_user_default_keywords: # Skip these subscriptions since the keyword was already # added to user's default lists. continue else: subscription.keywords.add(new_keyword)
def get_team(self): team = get_or_none(Team, slug=self.team_slug) if not team: self.error('Team with the slug "{}" does not exist.'.format( self.team_slug)) return return team
def get_maintainer_extra(developer_email, package_name=None): """ The function returns a list of additional items that are to be included in the general panel next to the maintainer. This includes: - Whether the maintainer agrees with lowthreshold NMU - Whether the maintainer is a Debian Maintainer """ developer = get_or_none(DebianContributor, email__email__iexact=developer_email) if not developer: # Debian does not have any extra information to include in this case. return None extra = [] if developer.agree_with_low_threshold_nmu: extra.append({ 'display': 'LowNMU', 'description': 'maintainer agrees with Low Threshold NMU', 'link': 'https://wiki.debian.org/LowThresholdNmu', }) if package_name and developer.is_debian_maintainer: if package_name in developer.allowed_packages: extra.append({ 'display': 'dm', }) return extra
def get_appstream_url(self): """ Returns the AppStream URL for the package matching the :class:`AppStreamStats <distro_tracker.vendor.debian.models.AppStreamHints>`. """ package = get_or_none(SourcePackageName, pk=self.package.pk) if not package: return '' maintainer_email = '' if package.main_version: maintainer = package.main_version.maintainer if maintainer: maintainer_email = maintainer.email # Adapt the maintainer URL to the form expected by appstream.d.o pkg_maintainer_email = re.sub( r"""[àáèéëêòöøîìùñ~/\(\)" ']""", '_', maintainer_email) if not package.main_version: return '' # TODO: What is the proper way to get (guess?) the archive-component vai the source-pkg here? section = "main" return ( 'https://appstream.debian.org/sid/{section}/issues/index.html#{maintainer}'.format( section=section, maintainer=pkg_maintainer_email) )
def send_to_subscribers(received_message, package_name, keyword): """ Sends the given email message to all subscribers of the package with the given name and those that accept messages tagged with the given keyword. :param received_message: The modified received package message to be sent to the subscribers. :type received_message: :py:class:`email.message.Message` or an equivalent interface object :param package_name: The name of the package for which this message was intended. :type package_name: string :param keyword: The keyword with which the message should be tagged :type keyword: string """ # Make a copy of the message to be sent and add any headers which are # specific for users that are directly subscribed to the package. received_message = deepcopy(received_message) add_direct_subscription_headers(received_message, package_name, keyword) package = get_or_none(PackageName, name=package_name) if not package: return # Build a list of all messages to be sent date = timezone.now().date() messages_to_send = [ prepare_message(received_message, subscription.email_settings.user_email.email, date) for subscription in package.subscription_set.all_active(keyword) ] send_messages(messages_to_send, date)
def _get_binary_bug_stats(self, binary_name): bug_stats, implemented = vendor.call( 'get_binary_package_bug_stats', binary_name) if not implemented: # The vendor does not provide a custom list of bugs, so the default # is to display all bug info known for the package. stats = get_or_none( BinaryPackageBugStats, package__name=binary_name) if stats is not None: bug_stats = stats.stats if bug_stats is None: return # Try to get the URL to the bug tracker for the given categories for category in bug_stats: url, implemented = vendor.call( 'get_bug_tracker_url', binary_name, 'binary', category['category_name']) if not implemented: continue category['url'] = url # Include the total bug count and corresponding tracker URL all_bugs_url, implemented = vendor.call( 'get_bug_tracker_url', binary_name, 'binary', 'all') return { 'total_count': sum( category['bug_count'] for category in bug_stats), 'all_bugs_url': all_bugs_url, 'categories': bug_stats, }
def get_lintian_url(self, full=False): """ Returns the lintian URL for the package matching the :class:`LintianStats <distro_tracker.vendor.debian.models.LintianStats>`. :param full: Whether the URL should include the full lintian report or only the errors and warnings. :type full: Boolean """ package = get_or_none(SourcePackageName, pk=self.package.pk) if not package: return '' maintainer_email = '' if package.main_version: maintainer = package.main_version.maintainer if maintainer: maintainer_email = maintainer.email # Adapt the maintainer URL to the form expected by lintian.debian.org lintian_maintainer_email = re.sub( r"""[àáèéëêòöøîìùñ~/\(\)" ']""", '_', maintainer_email) report = 'full' if full else 'maintainer' return ( 'https://lintian.debian.org/{report}/' '{maintainer}.html#{pkg}'.format( report=report, maintainer=lintian_maintainer_email, pkg=self.package) )
def _remove_subscriptions(self, email): """ Removes subscriptions for the given email. :param email: Email for which to remove all subscriptions. :type email: string :returns: A message explaining the result of the operation. :rtype: string """ user = get_or_none(UserEmail, email=email) if not user: return ('Email {email} is not subscribed to any packages. ' 'Bad email?'.format(email=email)) email_settings, _ = EmailSettings.objects.get_or_create(user_email=user) if email_settings.packagename_set.count() == 0: return 'Email {email} is not subscribed to any packages.'.format( email=email) out = [ 'Unsubscribing {email} from {package}'.format( email=email, package=package) for package in email_settings.packagename_set.all() ] email_settings.unsubscribe_all() return '\n'.join(out)
def pre_confirm(self): """ Implementation of a hook method which is executed instead of :py:meth:`handle` when the command is not confirmed. """ if not SourcePackageName.objects.exists_with_name(self.package): if BinaryPackageName.objects.exists_with_name(self.package): binary_package = \ BinaryPackageName.objects.get_by_name(self.package) self.warning('%s is not a source package.', self.package) self.reply('%s is the source package ' 'for the %s binary package', binary_package.main_source_package_name, binary_package.name) self.package = binary_package.main_source_package_name.name else: self.warning('%s is neither a source package ' 'nor a binary package.', self.package) settings = get_or_none(EmailSettings, user_email__email__iexact=self.user_email) if not settings or not settings.is_subscribed_to(self.package): self.error("%s is not subscribed, you can't unsubscribe.", self.user_email) return False self.reply('A confirmation mail has been sent to %s', self.user_email) return True
def get_panel_items(self): if not isinstance(self.package, SourcePackageName): # Only source packages can have build log check info return has_experimental = False experimental_repo = get_or_none(Repository, name='experimental') if experimental_repo: has_experimental = experimental_repo.has_source_package_name( self.package.name) query_string = urlencode({'p': self.package.name}) try: self.package.build_logcheck_stats has_checks = True except: has_checks = False logcheck_url = "https://qa.debian.org/bls/packages/{hash}/{pkg}.html".format( hash=self.package.name[0], pkg=self.package.name) return [ TemplatePanelItem('debian/logcheck-links.html', { 'package_query_string': query_string, 'has_checks': has_checks, 'logcheck_url': logcheck_url, 'has_experimental': has_experimental, }) ]
def create_news(message, package, create_package=False, **kwargs): """ Create a news item from the given message. The created news parameters are: - title - the Subject of the message - content - the message content itself - content_type - message/rfc822 :param message: A message which should be turned into a news item. :type message: :class:`email.message.Message` :param package: The package for which this news item should be created. :type package: :class:`distro_tracker.core.models.PackageName` or a string. :returns: The created news item :rtype: :class:`distro_tracker.core.models.News` """ if not isinstance(package, PackageName): if create_package: package, _ = PackageName.objects.get_or_create(name=package) else: package = get_or_none(PackageName, name=package) if package is None: # Don't record news for non-existing packages return return EmailNews.objects.create_email_news(message, package, **kwargs)
def handle(self): user = get_or_none(UserEmail, email__iexact=self.user_email) email_settings = get_or_none(EmailSettings, user_email=user) if user is None or email_settings is None: return packages = [ subscription.package.name for subscription in email_settings.subscription_set.all() ] email_settings.unsubscribe_all() self.reply('All your subscriptions have been terminated:') self.list_reply( '{email} has been unsubscribed from {package}@{fqdn}'.format( email=self.user_email, package=package, fqdn=DISTRO_TRACKER_FQDN) for package in sorted(packages))
def get_panel_items(self): if not isinstance(self.package, SourcePackageName): # Only source packages can have build log check info return has_experimental = False experimental_repo = get_or_none(Repository, suite='experimental') if experimental_repo: has_experimental = experimental_repo.has_source_package_name( self.package.name) query_string = urlencode({'p': self.package.name}) try: self.package.build_logcheck_stats has_checks = True except BuildLogCheckStats.DoesNotExist: has_checks = False logcheck_url = \ "https://qa.debian.org/bls/packages/{hash}/{pkg}.html".format( hash=urlquote(self.package.name[0], safe=""), pkg=urlquote(self.package.name, safe="")) try: infos = self.package.data.get(key='reproducibility') has_reproducibility = True reproducibility_status = infos.value['reproducibility'] except PackageData.DoesNotExist: has_reproducibility = False reproducibility_status = None reproducibility_url = \ "https://tests.reproducible-builds.org/debian/rb-pkg/{}.html" reproducibility_url = reproducibility_url.format( urlquote(self.package.name, safe="")) try: self.package.data.get(key='dependency_satisfaction') has_debcheck = True except PackageData.DoesNotExist: has_debcheck = False debcheck_url = \ "https://qa.debian.org/dose/debcheck/unstable_main/latest/src" \ "/{}.html".format(urlquote(self.package.name, safe="")) return [ TemplatePanelItem('debian/logcheck-links.html', { 'package_query_string': query_string, 'has_checks': has_checks, 'logcheck_url': logcheck_url, 'has_reproducibility': has_reproducibility, 'reproducibility_url': reproducibility_url, 'reproducibility_status': reproducibility_status, 'has_experimental': has_experimental, 'has_debcheck': has_debcheck, 'debcheck_url': debcheck_url, }) ]
def pre_confirm(self): """ Implementation of a hook method which is executed instead of :py:meth:`handle` when the command is not confirmed. """ user = get_or_none(UserEmail, email=self.user_email) emailsettings = get_or_none(EmailSettings, user_email=user) if user and emailsettings.is_subscribed_to(self.package): self.warn('{email} is already subscribed to {package}'.format( email=self.user_email, package=self.package)) return False if not SourcePackageName.objects.exists_with_name(self.package): if BinaryPackageName.objects.exists_with_name(self.package): binary_package = \ BinaryPackageName.objects.get_by_name(self.package) self.warn('{package} is not a source package.'.format( package=self.package)) self.reply('{package} is the source package ' 'for the {binary} binary package'.format( package=binary_package.main_source_package_name, binary=binary_package.name)) self.package = binary_package.main_source_package_name.name else: self.warn( '{package} is neither a source package ' 'nor a binary package.'.format(package=self.package)) if PseudoPackageName.objects.exists_with_name(self.package): self.warn('Package {package} is a pseudo package.'.format( package=self.package)) else: self.warn('Package {package} is not even a pseudo ' 'package.'.format(package=self.package)) Subscription.objects.create_for( email=self.user_email, package_name=self.package, active=False) self.reply('A confirmation mail has been sent to ' + self.user_email) return True
def get_team_and_user(self): team = get_or_none(Team, slug=self.team_slug) if not team: self.error('Team with the slug "{}" does not exist.'.format( self.team_slug)) return user_email, _ = UserEmail.objects.get_or_create(email=self.user_email) if user_email not in team.members.all(): self.warn("You are not a member of the team.") return return team, user_email
def get_policy_version(self): """ :returns: The latest version of the ``debian-policy`` package. """ debian_policy = get_or_none(SourcePackageName, name='debian-policy') if not debian_policy: return policy_version = debian_policy.main_version.version # Minor patch level should be disregarded for the comparison policy_version, _ = policy_version.rsplit('.', 1) return policy_version
def get(self, request, slug): self.request = request team = self.get_team(slug) if 'package' not in request.GET: raise Http404 package_name = request.GET['package'] package = get_or_none(PackageName, name=package_name) return render(self.request, self.template_name, { 'package': package, 'team': team, })
def pre_confirm(self): """ Implementation of a hook method which is executed instead of :py:meth:`handle` when the command is not confirmed. """ settings = get_or_none(EmailSettings, user_email__email__iexact=self.user_email) if not settings or settings.subscription_set.count() == 0: self.warning('User %s is not subscribed to any packages', self.user_email) return False self.reply('A confirmation mail has been sent to %s', self.user_email) return True
def send_to_team(received_message, team, keyword, package_name=None): """Send a message to a team.""" keyword = get_or_none(Keyword, name=keyword) package = get_or_none(PackageName, name=package_name) date = timezone.now().date() messages_to_send = [] logger.info('dispatch :: sending to team %s', team.slug) team_message = deepcopy(received_message) add_team_membership_headers(team_message, keyword.name, team) # Send the message to each member of the team for membership in team.team_membership_set.all(): # Do not send messages to muted memberships if membership.is_muted(package): continue # Do not send the message if the user has disabled the keyword if keyword not in membership.get_keywords(package): continue messages_to_send.append(prepare_message( team_message, membership.user_email.email, date)) send_messages(messages_to_send, date)
def get_uploader_extra(developer_email, package_name=None): """ The function returns a list of additional items that are to be included in the general panel next to an uploader. This includes: - Whether the uploader is a DebianMaintainer """ developer = get_or_none(DebianContributor, email__email__iexact=developer_email) extra = [] _add_dmd_entry(extra, developer_email) _add_dm_entry(extra, developer, package_name) return extra
def keyword_name_to_object(self, keyword_name): """ Takes a keyword name and returns a :py:class:`Keyword <distro_tracker.core.models.Keyword>` object with the given name if it exists. If not, a warning is added to the commands' output. :param keyword_name: The name of the keyword to be retrieved. :rtype: :py:class:`Keyword <distro_tracker.core.models.Keyword>` or ``None`` """ keyword = get_or_none(Keyword, name=keyword_name) if not keyword: self.warn('{keyword} is not a valid keyword'.format( keyword=keyword_name)) return keyword
def handle(self): package = get_or_none(PackageName, name=self.package_name) if not package: self.error('Package %s does not exist', self.package_name) return if package.subscriptions.count() == 0: self.reply('Package %s does not have any subscribers', package.name) return self.reply("Here's the list of subscribers to package %s:", self.package_name) self.list_reply( self.obfuscate(subscriber) for subscriber in package.subscriptions.all() )
def pre_login(form): """ If the user has a @debian.org email associated, don't let him log in directly through local authentication. """ username = form.cleaned_data.get('username') user_email = get_or_none(UserEmail, email=username) emails = [username] if user_email and user_email.user: emails += [x.email for x in user_email.user.emails.all()] if any(email.endswith('@debian.org') for email in emails): raise forms.ValidationError(mark_safe( "Your account has a @debian.org email address associated. " "To log in to the package tracker, you must use a SSL client " "certificate generated on " "<a href='https://sso.debian.org/spkac/enroll/'>" "sso.debian.org</a> (click on the link!)."))
def get_team_and_user(self): team = get_or_none(Team, slug=self.team_slug) if not team: self.error('Team with the slug "%s" does not exist.', self.team_slug) return if not team.public: self.error("The given team is not public. " "Please contact %s if you wish to join", team.owner.main_email) return user_email, _ = UserEmail.objects.get_or_create(email=self.user_email) if user_email in team.members.all(): self.warning("You are already a member of the team.") return return team, user_email
def process(message): """ Process an incoming message which is potentially a news item. The function first tries to call the vendor-provided function :func:`create_news_from_email_message <distro_tracker.vendor.skeleton.rules.create_news_from_email_message>`. If this function does not exist a news item is created only if there is a ``X-Distro-Tracker-Package`` header set giving the name of an existing source or pseudo package. If the ``X-Distro-Tracker-Url`` is also set then the content of the message will not be the email content, rather the URL given in this header. :param message: The received message :type message: :class:`bytes` """ assert isinstance(message, bytes), 'Message must be given as bytes' msg = message_from_bytes(message) # Try asking the vendor function first. created, implemented = vendor.call('create_news_from_email_message', msg) if implemented and created: return # If the message has an X-Distro-Tracker-Package header, it is # automatically made into a news item. if 'X-Distro-Tracker-Package' in msg: package_name = msg['X-Distro-Tracker-Package'] package = get_or_none(PackageName, name=package_name) if not package: return if 'X-Distro-Tracker-Url' not in msg: create_news(msg, package) else: distro_tracker_url = msg['X-Distro-Tracker-Url'] News.objects.create( title=distro_tracker_url, content="<a href={url}>{url}</a>".format( url=escape(distro_tracker_url)), package=package, content_type='text/html')