Exemplo n.º 1
0
    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
Exemplo n.º 2
0
    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)
        )
Exemplo n.º 3
0
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
Exemplo n.º 4
0
 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
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
    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,
        }
Exemplo n.º 7
0
    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)
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
    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
Exemplo n.º 10
0
    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
Exemplo n.º 11
0
    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,
        })
Exemplo n.º 12
0
    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
Exemplo n.º 13
0
    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
Exemplo n.º 14
0
 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))
Exemplo n.º 15
0
    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
Exemplo n.º 16
0
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',
            }]
Exemplo n.º 17
0
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()
    ]
Exemplo n.º 18
0
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')
Exemplo n.º 19
0
    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()
        )
Exemplo n.º 20
0
    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)
Exemplo n.º 21
0
    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)
Exemplo n.º 23
0
    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)
Exemplo n.º 25
0
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),
    }
Exemplo n.º 26
0
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')
        ]