def get_subscription(self, email, package_name): """ Helper method returning a :py:class:`Subscription <pts.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. """ email_user = get_or_none(EmailUser, email=email) if not email_user: 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_user=email_user) if not subscription: self.error_not_subscribed(email, package_name) return subscription
def get_lintian_url(self, full=False): """ Returns the lintian URL for the package matching the :class:`LintianStats <pts.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 # First 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 ( 'http://lintian.debian.org/{report}/{maintainer}.html#{pkg}'.format( report=report, maintainer=lintian_maintainer_email, pkg=self.package) )
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=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': 'http://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_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 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_user.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 add_keyword_to_subscriptions(self, new_keyword, existing_keyword): """ Adds the given ``new_keyword`` to each :py:class:`Subscription <pts.core.models.Subscription>`'s keywords list which already contains the ``existing_keyword``. :param new_keyword: The keyword to add to the :py:class:`Subscription <pts.core.models.Subscription>`'s keywords :type new_keyword: :py:class:`Keyword <pts.core.models.Keyword>` :param existing_keyword: The keyword or name of the keyword based on which all :py:class:`Subscription <pts.core.models.Subscription>` to which the ``new_keyword`` should be added are chosen. :type existing_keyword: :py:class:`Keyword <pts.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, EmailUser.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 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: 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.email_user.email, date)) send_messages(messages_to_send, date)
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 email_user, _ = EmailUser.objects.get_or_create(email=self.user_email) if email_user not in team.members.all(): self.warn("You are not a member of the team.") return return team, email_user
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. """ user = get_or_none(EmailUser, email=self.user_email) if not user or user.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 keyword_name_to_object(self, keyword_name): """ Takes a keyword name and returns a :py:class:`Keyword <pts.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 <pts.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): user = get_or_none(EmailUser, email=self.user_email) if user is None: return packages = [ subscription.package.name for subscription in user.subscription_set.all() ] user.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=PTS_FQDN) for package in sorted(packages))
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 if not team.public: self.error( "The given team is not public. " "Please contact {} if you wish to join".format( team.owner.main_email)) return email_user, _ = EmailUser.objects.get_or_create(email=self.user_email) if email_user in team.members.all(): self.warn("You are already a member of the team.") return return team, email_user
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 """ if package_name is None: return developer = get_or_none(DebianContributor, email__email=developer_email) if not developer: return if developer.is_debian_maintainer: if package_name in developer.allowed_packages: return [{ 'display': 'dm', }]
def get_binary_package_bug_stats(binary_name): """ Returns the bug statistics for the given binary package. Debian's implementation filters out some of the stored bug category stats. It also provides a different, more verbose, display name for each of them. The included categories and their names are: - rc - critical, grave serious - normal - important and normal - wishlist - wishlist and minor - fixed - pending and fixed """ stats = get_or_none(BinaryPackageBugStats, package__name=binary_name) if stats is None: return category_descriptions = { 'rc': { 'display_name': 'critical, grave and serious', }, 'normal': { 'display_name': 'important and normal', }, 'wishlist': { 'display_name': 'wishlist and minor', }, 'fixed': { 'display_name': 'pending and fixed', }, } def extend_category(category, extra_parameters): category.update(extra_parameters) return category # Filter the bug stats to only include some categories and add a custom # display name for each of them. return [ extend_category(category, category_descriptions[category['category_name']]) for category in stats.stats if category['category_name'] in category_descriptions.keys() ]
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 <pts.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-PTS-Package`` header set giving the name of an existing source or pseudo package. If the ``X-PTS-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-PTS-Package header, it is automatically made into # a news item. if 'X-PTS-Package' in msg: package_name = msg['X-PTS-Package'] package = get_or_none(PackageName, name=package_name) if not package: return if 'X-PTS-Url' not in msg: create_news(msg, package) else: pts_url = msg['X-PTS-Url'] News.objects.create( title=pts_url, content="<a href={url}>{url}</a>".format(url=escape(pts_url)), package=package, content_type='text/html')
def handle(self): package = get_or_none(PackageName, name=self.package_name) if not package: self.error('Package {package} does not exist'.format( package=self.package_name)) return if package.subscriptions.count() == 0: self.reply( 'Package {package} does not have any subscribers'.format( package=package.name)) return self.reply( "Here's the list of subscribers to package {package}:".format( package=self.package_name)) self.list_reply( self.obfuscate(subscriber) for subscriber in package.subscriptions.all() )
def post(self, request, slug): """ Removes the package given in the POST parameters from the team. If the currently logged in user is not a team member, a 403 Forbidden response is given. Once the package is removed, the user is redirected back to the team's page. """ self.request = request team = self.get_team(slug) if 'package' in request.POST: package_name = request.POST['package'] package = get_or_none(PackageName, name=package_name) if package: team.packages.remove(package) return redirect(team)
def handle(self): from pts.mail.control.commands import CommandFactory, CommandProcessor command_confirmation = get_or_none( CommandConfirmation, confirmation_key=self.confirmation_key) if not command_confirmation: self.error('Confirmation failed: Unknown key') return lines = command_confirmation.commands.splitlines() processor = CommandProcessor(CommandFactory({}), confirmed=True) processor.process(lines) if processor.is_success(): self.reply('Successfully confirmed commands.') self.reply(processor.get_output()) else: self.error('No commands confirmed') self.reply(processor.get_output()) command_confirmation.delete()
def handle(self, *args, **kwargs): self.verbose = int(kwargs.get('verbosity', 1)) > 1 inactive = kwargs['inactive'] self.out_packages = {} if len(args) == 0: for package in PackageName.objects.all(): self.output_package(package, inactive) else: for package_name in args: package = get_or_none(PackageName, name=package_name) if package: self.output_package(package, inactive) else: self.warn("{package} does not exist.".format( package=str(package_name))) format = 'default' if kwargs['json']: format = 'json' elif kwargs.get('udd_format', False): format = 'udd' return self.render_packages(format)
def post(self, request, slug): """ Adds the package given in the POST parameters to the team. If the currently logged in user is not a team member, a 403 Forbidden response is given. Once the package is added, the user is redirected back to the team's page. """ team = get_object_or_404(Team, slug=slug) if not team.user_is_member(request.user): # Only team mebers are allowed to modify the packages followed by # the team. raise PermissionDenied if 'package' in request.POST: package_name = request.POST['package'] package = get_or_none(PackageName, name=package_name) if package: team.packages.add(package) return redirect(team)
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(EmailUser, email=email) if not user: return ('Email {email} is not subscribed to any packages. ' 'Bad email?'.format(email=email)) if user.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 user.packagename_set.all() ] user.unsubscribe_all() return '\n'.join(out)
def get_bug_panel_stats(package_name): """ Returns bug statistics which are to be displayed in the bugs panel (:class:`BugsPanel <pts.core.panels.BugsPanel>`). Debian wants to include the merged bug count for each bug category (but only if the count is different than non-merged bug count) so this function is used in conjunction with a custom bug panel template which displays this bug count in parentheses next to the non-merged count. Each bug category count (merged and non-merged) is linked to a URL in the BTS which displays more information about the bugs found in that category. A verbose name is included for each of the categories. The function includes a URL to a bug history graph which is displayed in the rendered template. """ bug_stats = get_or_none(PackageBugStats, package__name=package_name) if not bug_stats: return # Map category names to their bug panel display names and descriptions category_descriptions = { 'rc': { 'display_name': 'RC', 'description': 'Release Critical', }, 'normal': { 'display_name': 'I&N', 'description': 'Important and Normal', }, 'wishlist': { 'display_name': 'M&W', 'description': 'Minor and Wishlist', }, 'fixed': { 'display_name': 'F&P', 'description': 'Fixed and Pending', }, 'gift': { 'display_name': 'gift', } } # Some bug categories should not be included in the count. exclude_from_count = ('gift',) stats = bug_stats.stats categories = [] total, total_merged = 0, 0 # From all known bug stats, extract only the ones relevant for the panel for category in stats: category_name = category['category_name'] if category_name not in category_descriptions.keys(): continue # Add main bug count category_stats = { 'category_name': category['category_name'], 'bug_count': category['bug_count'], } # Add merged bug count if 'merged_count' in category: if category['merged_count'] != category['bug_count']: category_stats['merged'] = { 'bug_count': category['merged_count'], } # Add descriptions category_stats.update(category_descriptions[category_name]) categories.append(category_stats) # Keep a running total of all and all-merged bugs if category_name not in exclude_from_count: total += category['bug_count'] total_merged += category.get('merged_count', 0) # Add another "category" with the bug totals. all_category = { 'category_name': 'all', 'display_name': 'all', 'bug_count': total, } if total != total_merged: all_category['merged'] = { 'bug_count': total_merged, } # The totals are the first displayed row. categories.insert(0, all_category) # Add URLs for all categories for category in categories: # URL for the non-merged category url = get_bug_tracker_url( package_name, 'source', category['category_name']) category['url'] = url # URL for the merged category if 'merged' in category: url_merged = get_bug_tracker_url( package_name, 'source', category['category_name'] + '-merged') category['merged']['url'] = url_merged # Debian also includes a custom graph of bug history graph_url = ( 'http://qa.debian.org/data/bts/graphs/{package_hash}/{package_name}.png' ) if package_name.startswith('lib'): package_hash = package_name[:4] else: package_hash = package_name[0] # Final context variables which are available in the template return { 'categories': categories, 'graph_url': graph_url.format( package_hash=package_hash, package_name=package_name), }
def create_news_from_email_message(message): """ In Debian's implementation, this function creates news when the received mail's origin is either the testing watch or katie. """ subject = message.get("Subject", None) if not subject: return subject_words = subject.split() # Source upload? if len(subject_words) > 1 and subject_words[0] in ('Accepted', 'Installed'): if 'source' not in subject: # Only source uploads should be considered. return package_name = subject_words[1] package = get_or_none(SourcePackageName, name=package_name) if package: return [EmailNews.objects.create_email_news(message, package)] # DAK rm? elif 'X-DAK' in message: x_dak = message['X-DAK'] katie = x_dak.split()[1] if katie != 'rm': # Only rm mails are processed. return body = get_decoded_message_payload(message) if not body: # The content cannot be decoded. return # Find all lines giving information about removed source packages re_rmline = re.compile(r"^\s*(\S+)\s*\|\s*(\S+)\s*\|.*source", re.M) source_removals = re_rmline.findall(body) # Find the suite from which the packages have been removed suite = re.search(r"have been removed from (\S+):", body).group(1) news_from = message.get('Sender', '') # Add a news item for each source removal. created_news = [] for removal in source_removals: package_name, version = removal package = get_or_none(SourcePackageName, name=package_name) if not package: # This package is not tracked by the PTS continue title = "Removed {ver} from {suite}".format(ver=version, suite=suite) created_news.append(EmailNews.objects.create_email_news( title=title, message=message, package=package, created_by=news_from)) return created_news # Testing Watch? elif 'X-Testing-Watch-Package' in message: package_name = message['X-Testing-Watch-Package'] package = get_or_none(SourcePackageName, name=package_name) if not package: # This package is not tracked by the PTS return title = message.get('Subject', '') if not title: title = 'Testing Watch Message' return [ EmailNews.objects.create_email_news( title=title, message=message, package=package, created_by='Britney') ]