class Package(models.Model):
    PYPI = 'pypi'
    PROVIDER_CHOICES = (
        (PYPI, 'PyPi'),
    )

    name = models.CharField(_('name'), max_length=255, unique=True)
    normalized_name = models.CharField(_('Normalized Name'), max_length=255,
                                       unique=True)
    url = models.URLField(_('url'))
    provider = models.CharField(_('provider'), max_length=255,
        choices=PROVIDER_CHOICES, default=PYPI)
    initial_sync_done = models.BooleanField(default=False)

    @classmethod
    def create_with_provider_url(cls, name, provider='pypi', url=None):
        if url is None:
            url = urlparse.urljoin(DEFAULT_SERVER, name)
        pkg = cls(name=name, url=url, provider=provider)
        pkg.save()
        return pkg

    def sync_versions(self):
        if self.initial_sync_done:
            return
        client = CheeseShop()
        versions = client.get_package_versions(self.name)
        for version in versions:
            urls = client.get_release_urls(self.name, version)
            if urls:
                url = urls[0]
                utime = time.mktime(url['upload_time'].timetuple())
                release_date = make_aware(
                    datetime.datetime.fromtimestamp(utime),
                    pytz.UTC)
                PackageVersion.objects.get_or_create(
                    package=self,
                    version=version,
                    release_date=release_date)
        self.initial_sync_done = True
        self.save()

    def save(self, *args, **kwargs):
        if not self.normalized_name:
            self.normalized_name = normalize_name(self.name)
        super(Package, self).save(*args, **kwargs)

    class Meta:
        verbose_name = _('package')
        verbose_name_plural = _('packages')
        unique_together = ('name', 'provider')

    def __unicode__(self):
        return self.name

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self.name) class PackageVersion(models.Model):
    package = models.ForeignKey(Package, verbose_name=_('package'),
                                related_name='versions')
    version = models.CharField(_('version'), max_length=255)
    release_date = models.DateTimeField(_('release date'))

    class Meta:
        verbose_name = _('package version')
        verbose_name_plural = _('package versions')
        unique_together = ('package', 'version')

    def __unicode__(self):
        return '{}{}'.format(self.package.name, self.version) class Project(models.Model):
    name = models.CharField(_('name'), max_length=255)
    slug = models.SlugField(_('slug'), unique=True)
    members = models.ManyToManyField(User, through=ProjectMember,
          verbose_name=_('members'))

    class Meta:
        verbose_name = _('project')
        verbose_name_plural = _('projects')

    @models.permalink
    def get_absolute_url(self):
        return 'folivora_project_detail', (), {'slug': self.slug}

    def create_logentry(self, type, action, user=None, **kwargs):
        Log.objects.create(project=self, type=type,
                           action=action, data=kwargs, user=user)

    @property
    def requirements(self):
        query = ProjectDependency.objects.filter(project=self) \
                                         .select_related('package') \
                                         .order_by('package__name')
        return "\n".join([d.dependency_string for d in query])

    @property
    def requirement_dict(self):
        query = ProjectDependency.objects.filter(project=self) \
                                         .select_related('package')
        return dict((d.package.name, d.version) for d in query)

    def process_changes(self, user, remove=None, change=None, add=None):
        log_entries = []

        remove = remove if remove else []
        change = change if change else []
        add = add if add else []

        for package_id, version in add:
            log_entries.append(Log(type='project_dependency', action='add',
                                   project_id=self.pk, package_id=package_id,
                                   user=user, data={'version': version}))
        for package_id, version in remove:
            log_entries.append(Log(type='project_dependency', action='remove',
                                   project_id=self.pk, package_id=package_id,
                                   user=user, data={'version': version}))
        for package_id, old_version, new_version in change:
            log_entries.append(Log(type='project_dependency', action='update',
                                   project_id=self.pk, package_id=package_id,
                                   user=user, data={'version': new_version,
                                         'old_version': old_version}))

        Log.objects.bulk_create(log_entries)
        from .tasks import sync_project
        # give the request time to finish before syncing
        sync_project.apply_async(args=[self.pk], countdown=1)

    @property
    def owners(self):
        return self.members.filter(projectmember__state=ProjectMember.OWNER) class Log(models.Model):
    project = models.ForeignKey(Project, verbose_name=_('project'))
    package = models.ForeignKey(Package, verbose_name=_('package'),
                                null=True, blank=True, default=None)
    user = models.ForeignKey(User, verbose_name=_('user'), null=True)
    when = models.DateTimeField(_('when'), default=now)
    action = models.CharField(_('action'), max_length=255)
    type = models.CharField(_('type'), max_length=255)
    data = hstore.DictionaryField()

    objects = hstore.HStoreManager()

    class Meta:
        verbose_name = _('log')
        verbose_name_plural = _('logs')

    def __unicode__(self):
        return '{}.{}, package: {}, project: {}'.format(self.type, self.action,
                                                        self.package_id,
                                                        self.project_id)

    @property
    def template(self):
        return 'folivora/notifications/{}.{}.html'.format(self.type,
                                                          self.action) class ProjectDependency(models.Model):
    project = models.ForeignKey(Project, verbose_name=_('project'),
                                related_name='dependencies')
    package = models.ForeignKey(Package, verbose_name=_('package'))
    version = models.CharField(_('version'), max_length=255)
    update = models.ForeignKey(PackageVersion, verbose_name=_('update'),
                               null=True, blank=True, default=None)

    class Meta:
        verbose_name = _('project dependency')
        verbose_name_plural = _('project dependencies')
        unique_together = ('project', 'package')

    @property
    def dependency_string(self):
        return u"%s==%s" % (self.package.name, self.version)

    @property
    def update_available(self):
        return self.update_id is not None

    @classmethod
    def process_formset(cls, formset, original_data, user):
        remove = []
        change = []
        for instance in formset.deleted_objects:
            remove.append((instance.package.id, instance.version))
        for instance, d in formset.changed_objects:
            existing = original_data[instance.pk]
            change.append((instance.package.id,
                           existing.version,
                           instance.version))
        formset.instance.process_changes(user, remove, change) class ProjectMember(models.Model):
    OWNER = 0
    MEMBER = 1
    STATE_CHOICES = (
        (OWNER, _('Owner')),
        (MEMBER, _('Member'))
    )

    project = models.ForeignKey('Project', verbose_name=_('project'))
    user = models.ForeignKey(User, verbose_name=_('user'))
    state = models.IntegerField(_('state'), choices=STATE_CHOICES)
    mail = models.EmailField(_('Email'), max_length=255, blank=True)
    jabber = models.CharField(_('Jabber'), max_length=255, blank=True)

    class Meta:
        verbose_name = _('project member')
        verbose_name_plural = _('project members')
        unique_together = ('project', 'user') class SyncState(models.Model):
    """Generic model to store syncronization states."""
    CHANGELOG = 'changelog'

    TYPE_CHOICES = (
        (_('Changelog'), CHANGELOG),
    )

    STATE_DOWN = 'down'
    STATE_RUNNING = 'running'

    STATE_CHOICES = (
        (_('Down'), STATE_DOWN),
        (_('Running'), STATE_RUNNING),
    )

    type = models.CharField(max_length=255, choices=TYPE_CHOICES, unique=True)
    state = models.CharField(max_length=255, choices=STATE_CHOICES,
                             default=STATE_RUNNING)
    last_sync = models.DateTimeField(_('Last Sync'), default=now) def get_parser(name):
    for parser in PARSERS:
        if parser.name == name:
            return parser()
    raise ValueError('Parser %s does not exist' % name) class BaseParser(object):
    name = None
    title = None

    def parse(self, lines):
        raise NotImplementedError def is_valid_jid(jabber):
    return _jabber_re.match(jabber) is not None class JabberField(fields.CharField):
    def clean(self, value):
        value = value.strip()
        if not value and not self.required:
            return value
        if not is_valid_jid(value):
            raise ValidationError(_(u'The entered Jabber address is invalid. '
                u'Please check your input.'))
        return value class SortListMixin(object):
    """This mixin allows the user to sort the queryset by predefined fields.

    This mixin is used by assigning an iterable to `sort_fields` containing
    strings which have to match the field names of the model from the
    underlying generic view.

    The default order is specified by `default_order` which has to be a string
    with the same syntax requirements as Django's `QuerySet.order_by()` method.

    Incoming requests are checked for a `sort`-GET param and verified against
    `sort_fields` before the sorting is applied.
    """
    sort_fields = None
    default_order = ('pk',)

    def get_context_data(self, **kwargs):
        context = super(SortListMixin, self).get_context_data(**kwargs)
        context.update({
            'sort_fields': self.sort_fields,
            'sort_field': self.sort_field,
            'sort_order': self.sort_order
        })
        return context

    def get_queryset(self):
        qs = super(SortListMixin, self).get_queryset()
        self.sort_field = None
        if self.sort_fields:
            sort = self.request.GET.get('sort')
            if sort and sort.strip('-') in self.sort_fields:
                self.sort_field = sort
                qs = qs.order_by(sort)
            else:
                qs = qs.order_by(*self.default_order)
                self.sort_field = self.default_order[0]
        self.sort_order = 'desc' if self.sort_field[0] == '-' else 'asc'
        self.sort_field = self.sort_field.strip('-')
        return qs
import socket
import logging
import pytz
import mock
from datetime import datetime
from django.conf import settings
from django.core.files.base import ContentFile
from django.core import mail
from django.test import TestCase
from django.utils.timezone import make_aware
from django.test.client import Client
from django.test.utils import override_settings
from django.contrib.auth.models import User
from .models import (Package, PackageVersion, Project, Log,
    ProjectDependency, ProjectMember, SyncState)
from . import tasks
from .utils.parsers import get_parser, BaseParser
from .utils.jabber import is_valid_jid
from .utils.forms import JabberField
from .utils.views import SortListMixin







test_logger = logging.getLogger(__name__)
test_handler = logging.NullHandler()
test_handler.setLevel(logging.DEBUG)
test_logger.addHandler(test_handler)


class CheesyMock(object):

    def get_package_list(self):
        return ['pmxbot', 'gunicorn']

    def get_changelog(self, hours, force=False):
        return [['pmxbot', '1101.8.1', 1345259834, 'new release'],
                ['pmxbot2', '1101.8.1', 1345259834, 'new release'],
                ['gunicorn', '0.14.6', 1345259834, 'remove'],
                ['gunicorn-del', None, 1345259834, 'remove'],
                ['new_package', '0.1', 1345259834, 'new release'],
                ['created package', '0.1', 1345259834, 'create']]

    def get_release_urls(self, name, version):
        if name == 'gunicorn':
            return []
        return [{'comment_text': '',
                 'downloads': 0,
                 'filename': 'pmxbot-1101.8.1.zip',
                 'has_sig': False,
                 'md5_digest': '0a945fa5ea023036777b7cfde4518932',
                 'packagetype': 'sdist',
                 'python_version': 'source',
                 'size': 223006,
                 'upload_time': datetime(2012, 8, 18, 3, 17, 15),
                 'url': 'http://pypi.python.org/packages/source/p/pmxbot/pmxbot-1101.8.1.zip'}]

    def get_package_versions(self, name):
        if name in ('pmxbot', 'pmxbot2'):
            return ['1101.8.1']
        elif name == 'pytz':
            return ['2012d']
        else:
            return ['0']


class NotConnectedCheesyMock(object):
    def get_changelog(self, hours, force=False):
        err = socket.error()
        err.errno = 111
        raise err


def stub(*args, **kwargs):
    return


class TestPackageModel(TestCase):

    def setUp(self):
        pkg = Package.create_with_provider_url('pmxbot')
        project = Project.objects.create(name='test', slug='test')
        ProjectDependency.objects.create(
            project=project,
            package=pkg,
            version='1101.8.0')

    def test_creation(self):
        Package.objects.create(name='gunicorn',
                               url='http://pypi.python.org/pypi/gunicorn',
                               provider='pypi')
        pkg = Package.objects.get(name='gunicorn')
        self.assertEqual(pkg.name, 'gunicorn')
        self.assertEqual(pkg.url, 'http://pypi.python.org/pypi/gunicorn')
        self.assertEqual(pkg.provider, 'pypi')

    def test_creation_automatic_normalize(self):
        Package.objects.create(name='GuNiCoRn',
                               url='http://pypi.python.org/pypi/gunicorn',
                               provider='pypi')
        pkg = Package.objects.get(name='GuNiCoRn')
        self.assertEqual(pkg.name, 'GuNiCoRn')
        self.assertEqual(pkg.normalized_name, 'gunicorn')

    @mock.patch('folivora.models.CheeseShop', CheesyMock)
    def test_version_sync(self):
        pkg = Package.objects.get(name='pmxbot')
        self.assertEqual(pkg.versions.count(), 0)
        pkg.sync_versions()
        self.assertEqual(pkg.versions.count(), 1)
        version = pkg.versions.all()[0]
        self.assertEqual(version.version, '1101.8.1')
        self.assertNumQueries(0, pkg.sync_versions)

    @mock.patch('folivora.models.CheeseShop', CheesyMock)
    def test_version_sync_without_versions(self):
        #Bug group/337798
        pkg = Package.objects.create(name='gunicorn',
                                     url='http://pypi.python.org/pypi/gunicorn',
                                     provider='pypi')
        self.assertEqual(pkg.versions.count(), 0)
        pkg.sync_versions()
        self.assertEqual(pkg.versions.count(), 0)


class TestPackageVersionModel(TestCase):

    def test_creation(self):
        Package.objects.create(name='gunicorn',
                               url='http://pypi.python.org/pypi/gunicorn',
                               provider='pypi')
        pkg = Package.objects.get(name='gunicorn')
        dt = make_aware(datetime(2012, 7, 26, 23, 51, 18), pytz.UTC)
        PackageVersion.objects.create(package=pkg,
                                      version='0.14.6',
                                      release_date=dt)

        vers = PackageVersion.objects.get(package__name='gunicorn',