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 UserProfile(models.Model):
    user = models.OneToOneField(User)
    language = models.CharField(_('Language'), max_length=255,
                                choices=settings.LANGUAGES, blank=True)
    timezone = models.CharField(_('Timezone'), max_length=255, default='UTC')
    jabber = models.CharField(_('JID'), max_length=255, blank=True)

    def get_absolute_url(self):
        return reverse('folivora_profile_edit') 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 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 ModelForm(forms.LayoutRenderer, BaseModelForm):
    """Model form which does incorporate floppyforms layout renderer"""
    __metaclass__ = FloppyFormsModelMetaclass 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 def get_parser(name):
    for parser in PARSERS:
        if parser.name == name:
            return parser()
    raise ValueError('Parser %s does not exist' % name) def get_parser_choices():
    return [(parser.name, parser.title) for parser in PARSERS] def normalize_name(name):
    return pkg_resources.safe_name(name).lower() @task
def sync_project(project_pk):
    """Syncronize all dependencies of a project.

    This syncronizes all package versions and creates proper
    log entries on updates as well as starts the notification
    routing.
    """
    project = Project.objects.get(pk=project_pk)

    log_entries = []
    for dependency in project.dependencies.all():
        package = dependency.package
        package.sync_versions()
        versions = list(package.versions.values_list('version', flat=True))

        if versions:
            # We use LooseVersion since at least pytz fails with StrictVersion
            # TODO: tests
            versions.sort(key=LooseVersion)

            if LooseVersion(dependency.version) >= LooseVersion(versions[-1]):
                ProjectDependency.objects.filter(pk=dependency.pk) \
                                         .update(update=None)
                continue  # The dependency is up2date, nothing to do

            pv = PackageVersion.objects.get(package=package,
                                            version=versions[-1])

            if pv.pk == dependency.update_id:
                continue

            dependency.update = pv
            dependency.save()
            tz = timezone.utc
            since = str(timezone.make_naive(pv.release_date, tz))
            log_entries.append(Log(type='project_dependency',
                                   action='update_available',
                                   project=project, package=package,
                                   data={'version': versions[-1],
                                         'since': since}))
    if log_entries:
        Log.objects.bulk_create(log_entries)
        send_notifications(project, log_entries)
import pytz
import floppyforms as forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy, ugettext as _
from .models import (Project, UserProfile, Package, ProjectDependency,
    ProjectMember)
from .utils.forms import ModelForm, JabberField
from .utils.parsers import get_parser, get_parser_choices
from .utils.pypi import normalize_name
from .tasks import sync_project





TIMEZONES = pytz.common_timezones


class AddProjectForm(ModelForm):
    requirements = forms.FileField(required=False)
    parser = forms.ChoiceField(choices=get_parser_choices())

    class Meta:
        model = Project
        fields = ('name', 'slug')

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super(AddProjectForm, self).__init__(*args, **kwargs)

    def clean(self):
        cleaned_data = self.cleaned_data
        data = cleaned_data.get('requirements', None)
        project_deps = []
        if data and 'parser' in cleaned_data:
            parser = get_parser(cleaned_data['parser'])
            packages, missing = parser.parse(data)
            packages = dict((normalize_name(k), v) for k, v
                                                   in packages.iteritems())

            pkg_names = [normalize_name(name) for name in packages.keys()]

            known_packages = Package.objects \
                .filter(normalized_name__in=pkg_names) \
                .values_list('normalized_name', 'name', 'pk')
            known_package_names = map(lambda x: x[1], known_packages)

            # TODO: report missing back to the ui.
            normalized = (normalize_name(n) for n in known_package_names)
            missing.extend(set(pkg_names).difference(normalized))

            for normalized, name, pk in known_packages:
                project_deps.append(
                    ProjectDependency(package_id=pk,
                                      version=packages[normalized]))
        cleaned_data['requirements'] = project_deps
        return cleaned_data

    def save(self, commit=True):
        project = super(AddProjectForm, self).save(True)
        project.create_logentry('project', 'add', self.user, name=project.name)
        ProjectMember.objects.create(user=self.user, state=ProjectMember.OWNER,
            project=project)
        deps = self.cleaned_data['requirements']
        for dep in deps:
            dep.project = project
        ProjectDependency.objects.bulk_create(deps)
        sync_project.delay(project.pk)
        return project


class ProjectDependencyForm(ModelForm):
    class Meta:
        model = ProjectDependency
        fields = ('version', 'id')


class UpdateUserProfileForm(ModelForm):
    timezone = forms.ChoiceField(label=ugettext_lazy('Timezone'),
                                 required=True,
                                 choices=zip(TIMEZONES, TIMEZONES))
    jabber = JabberField(required=False)
    email = forms.EmailField(required=True)

    class Meta:
        model = UserProfile
        fields = ('email', 'jabber', 'language', 'timezone')

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance', None)
        if instance is not None:
            initial = kwargs.setdefault('initial', {})
            initial['email'] = instance.user.email

        super(UpdateUserProfileForm, self).__init__(*args, **kwargs)


class ProjectMemberForm(ModelForm):
    class Meta:
        model = ProjectMember
        fields = ('id', 'state')


class CreateProjectMemberForm(ModelForm):
    class Meta:
        model = ProjectMember
        fields = ('user', 'state')


class UpdateProjectDependencyForm(forms.Form):
    packages = forms.CharField(widget=forms.Textarea, required=False)
    parser = forms.ChoiceField(choices=get_parser_choices())

    def clean(self):
        cleaned_data = self.cleaned_data
        if 'packages' in cleaned_data and 'parser' in cleaned_data:
            data = cleaned_data['packages']

            parser = get_parser(cleaned_data['parser'])