Commits

Anonymous committed 2ed679b

Collections Nuke: First batch of changes.

- Add/Remove Project Releases works, views don't die.

Comments (0)

Files changed (18)

transifex/projects/__init__.py

     Look through the components for a specific project and update them based 
     on the project changes
     """
-    from projects.models import Component
-
-    collection_query = project.collections.values('pk').query
-    for c in Component.objects.filter(project__id=project.id):
-        releases = c.releases.exclude(collection__id__in=collection_query)
-        for release in releases:
-            logger.debug("Release '%s' removed from '%s'" % (release, c))
-            c.releases.remove(release)
-        c.save()
+    pass
 
 def _projectpostm2mhandler(sender, **kwargs):
     if 'instance' in kwargs:

transifex/projects/admin.py

 from projects.models import *
 from authority.admin import PermissionInline
 
-admin.site.register(Project, inlines=[PermissionInline])
-admin.site.register(Component)
+admin.site.register(Project, inlines=(PermissionInline,))
+admin.site.register(Component)
+admin.site.register(Release)

transifex/projects/forms.py

 
 from ajax_select.fields import AutoCompleteSelectMultipleField
 
-from projects.models import Project, Component
+from projects.models import Project, Component, Release
 from txcommon.validators import ValidRegexField
 
 class ProjectForm(forms.ModelForm):
         self.fields["project"].queryset = projects
         self.fields["project"].empty_label = None
 
-        # Filtering releases by the collections of the project
-        collection_query = project.collections.values('pk').query
-        releases = self.fields["releases"].queryset.filter(
-                                           collection__id__in=collection_query)
-        self.fields["releases"].queryset = releases
-
 
 class ComponentAllowSubForm(forms.ModelForm):
 
             self.fields["submission_type"].choices = \
                 settings.SUBMISSION_CHOICES[codebase_type].items()
 
+        
 
+class ReleaseForm(forms.ModelForm):
+
+    class Meta:
+        model = Release
+
+    def __init__(self, project, *args, **kwargs):
+        super(ReleaseForm, self).__init__(*args, **kwargs)
+        projects = self.fields["project"].queryset.filter(slug=project.slug)
+        self.fields["project"].queryset = projects
+        self.fields["project"].empty_label = None
+

transifex/projects/migrations/0005_addrelease.py

+# -*- coding: utf-8 -*-
+
+from south.db import db
+from django.db import models
+from projects.models import *
+from txcommon.db.models import IntegerTupleField
+
+class Migration:
+    
+    def forwards(self, orm):
+        
+        # Adding model 'Release'
+        db.create_table('projects_release', (
+            ('id', orm['projects.release:id']),
+            ('slug', orm['projects.release:slug']),
+            ('name', orm['projects.release:name']),
+            ('description', orm['projects.release:description']),
+            ('long_description', orm['projects.release:long_description']),
+            ('homepage', orm['projects.release:homepage']),
+            ('release_date', orm['projects.release:release_date']),
+            ('stringfreeze_date', orm['projects.release:stringfreeze_date']),
+            ('develfreeze_date', orm['projects.release:develfreeze_date']),
+            ('created', orm['projects.release:created']),
+            ('modified', orm['projects.release:modified']),
+            ('long_description_html', orm['projects.release:long_description_html']),
+            ('project', orm['projects.release:project']),
+        ))
+        db.send_create_signal('projects', ['Release'])
+        
+        # Adding ManyToManyField 'Component.releases'
+        db.create_table('projects_component_releases', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('component', models.ForeignKey(orm.Component, null=False)),
+            ('release', models.ForeignKey(orm.Release, null=False))
+        ))
+        
+        # Creating unique_together for [slug, project] on Release.
+        db.create_unique('projects_release', ['slug', 'project_id'])
+        
+    
+    
+    def backwards(self, orm):
+        
+        # Deleting unique_together for [slug, project] on Release.
+        db.delete_unique('projects_release', ['slug', 'project_id'])
+        
+        # Deleting model 'Release'
+        db.delete_table('projects_release')
+        
+        # Dropping ManyToManyField 'Component.releases'
+        db.delete_table('projects_component_releases')
+        
+    
+    
+    models = {
+        'auth.group': {
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)"},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'})
+        },
+        'codebases.unit': {
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_checkout': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'unique': 'True'}),
+            'root': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'languages.language': {
+            'Meta': {'db_table': "'translations_language'"},
+            'code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True'}),
+            'code_aliases': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'null': 'True'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True'}),
+            'nplurals': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+            'pluralequation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'specialchars': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
+        },
+        'projects.component': {
+            'Meta': {'unique_together': "(('project', 'slug'),)"},
+            '_unit': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['codebases.Unit']", 'unique': 'True', 'null': 'True', 'db_column': "'unit_id'", 'blank': 'True'}),
+            'allows_submission': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'file_filter': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'i18n_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'long_description': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'blank': 'True'}),
+            'long_description_html': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'blank': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'pofiles': ('django.contrib.contenttypes.generic.GenericRelation', [], {'to': "orm['translations.POFile']"}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']"}),
+            'releases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['projects.Release']", 'null': 'True', 'blank': 'True'}),
+            'should_calculate': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '30', 'db_index': 'True'}),
+            'source_lang': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'submission_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
+        },
+        'projects.project': {
+            'anyone_submit': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'bug_tracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'feed': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'long_description': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'blank': 'True'}),
+            'long_description_html': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'blank': 'True'}),
+            'maintainers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'null': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '30', 'unique': 'True', 'db_index': 'True'}),
+            'tags': ('TagField', [], {})
+        },
+        'projects.release': {
+            'Meta': {'unique_together': "(('slug', 'project'),)"},
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'develfreeze_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'long_description': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'blank': 'True'}),
+            'long_description_html': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'blank': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['projects.Project']"}),
+            'release_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '30', 'db_index': 'True'}),
+            'stringfreeze_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'translations.pofile': {
+            'Meta': {'unique_together': "(('content_type', 'object_id', 'filename'),)"},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'error': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'fuzzy': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'fuzzy_perc': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_msgmerged': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_pot': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
+            'language': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['languages.Language']", 'null': 'True'}),
+            'language_code': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'rev': ('IntegerTupleField', [], {'max_length': '64', 'null': 'True'}),
+            'total': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'trans': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'trans_perc': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'untrans': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'untrans_perc': ('django.db.models.fields.PositiveIntegerField', [], {'default': '100'})
+        }
+    }
+    
+    complete_apps = ['projects']

transifex/projects/models.py

 from codebases.models import Unit
 from vcs.models import VcsUnit
 from tarball.models import Tarball
-from txcollections.models import Collection, CollectionRelease
 from translations.models import POFile
 from txcommon.log import logger, log_model
 from txcommon.notifications import is_watched_by_user_signal
 from projects.handlers import get_trans_handler
 from projects import signals
+from releases.models import Release
 
 def cached_property(func):
     """
     tags = TagField(verbose_name=_('Tags'))
 
     # Relations
-    # The collections this project belongs to.
-    collections = models.ManyToManyField(Collection, 
-        verbose_name=_('Collections'), related_name='projects',
-        blank=True, null=True,)
     maintainers = models.ManyToManyField(User, verbose_name=_('Maintainers'),
         related_name='projects_maintaining', blank=False, null=True)
 
     project = models.ForeignKey(Project, verbose_name=_('Project'))
     _unit = models.OneToOneField(Unit, verbose_name=_('Unit'),
         blank=True, null=True, editable=False, db_column='unit_id')
-    pofiles = generic.GenericRelation(POFile)
-    releases = models.ManyToManyField(CollectionRelease,
+    releases = models.ManyToManyField('Release',
         verbose_name=_('Releases'), related_name='components',
         blank=True, null=True)
+    pofiles = generic.GenericRelation(POFile)
 
     # Managers
     objects = ComponentManager()
             pass
 
 log_model(Component)
+
+
+class Release(models.Model):
+
+    """
+    A release of a project, as in 'a set of specific components'.
+    
+    Represents the packaging and releasing of a software project (big or
+    small) on a particular date, for which makes sense to track
+    translations across the whole release.
+    
+    Examples of Releases is Transifex 1.0, GNOME 2.26, Fedora 10 etc.
+    """
+
+    slug = models.SlugField(_('Slug'), max_length=30,
+        help_text=_('A short label to be used in the URL, containing only '
+                    'letters, numbers, underscores or hyphens.'))
+    name = models.CharField(_('Name'), max_length=50,
+        help_text=_('A string like a name or very short description.'))
+    description = models.CharField(_('Description'),
+        blank=True, max_length=255,
+        help_text=_('A sentence or two describing the object.'))
+    long_description = models.TextField(_('Long description'),
+        blank=True, max_length=1000,
+        help_text=_('Use Markdown syntax.'))
+    homepage = models.URLField(blank=True, verify_exists=False)
+
+    release_date = models.DateTimeField(_('Release date'),
+        blank=True, null=True,
+        help_text=_('When this release will be available.'))
+    stringfreeze_date = models.DateTimeField(_('String freeze date'),
+        blank=True, null=True,
+        help_text=_("When the translatable strings will be frozen (no strings "
+                    "can be added/modified which affect translations."))
+    develfreeze_date = models.DateTimeField(_('Devel freeze date'),
+        blank=True, null=True,
+        help_text=_("The last date packages from this release can be built "
+                    "from the developers. Translations sent after this date "
+                    "will not be included in the released version."))
+    
+    created = models.DateTimeField(auto_now_add=True, editable=False)
+    modified = models.DateTimeField(auto_now=True, editable=False)
+    
+    # Normalized fields
+    long_description_html = models.TextField(_('HTML Description'),
+        blank=True, max_length=1000,
+         help_text=_('Description in HTML.'), editable=False)
+
+    # Relations
+    project = models.ForeignKey(Project, verbose_name=_('Project'), related_name='releases')
+
+    def __unicode__(self):
+        return self.name
+
+    def __repr__(self):
+        return _('<Release: %(rel)s (Project %(proj)s)>') % {
+            'rel': self.name,
+            'proj': self.project.name}
+    
+    @property
+    def full_name(self):
+        return "%s (%s)" % (self.name, self.project.name)
+
+    class Meta:
+        unique_together = ("slug", "project")
+        verbose_name = _('release')
+        verbose_name_plural = _('releases')
+        ordering  = ('name',)
+        get_latest_by = 'created'
+
+    def save(self, *args, **kwargs):
+        import markdown
+        from cgi import escape
+        desc_escaped = escape(self.long_description)
+        self.long_description_html = markdown.markdown(desc_escaped)
+        created = self.created
+        super(Release, self).save(*args, **kwargs)
+
+    @permalink
+    def get_absolute_url(self):
+        return ('release_detail', None,
+                { 'project_slug': self.project.slug,
+                 'release_slug': self.slug })
+
+log_model(Release)

transifex/projects/permissions/__init__.py

     ('granular', 'project_perm.maintain'),
     ('general',  'repowatch.add_watch'),
     ('general',  'repowatch.delete_watch'),
-)
+)
+
+
+# Release permissions required
+
+pr_release_add_change = (
+    ('granular', 'project_perm.maintain'),
+    ('general',  'projects.add_release'),
+    ('general',  'projects.change_release'),
+)
+
+pr_release_delete = (
+    ('granular', 'project_perm.maintain'),
+    ('general',  'projects.delete_release'),
+)

transifex/projects/tests.py

 import unittest
+from projects.models import (Project, Release)
 
 class SmokeTestCase(unittest.TestCase):
     """Test that all project URLs return correct status code."""
                 '/projects/feeds/foob4r/',
                 '/account/foob4r/',]}
 
-    def testStatusCode(self):
+    def atestStatusCode(self):
         from django.test.client import Client
         client = Client()
         for expected_code in self.pages.keys():
                 self.assertEquals(page.status_code, expected_code,
                     "Status code for page '%s' was %s instead of %s" %
                     (page_url, page.status_code, expected_code))
+
+
+class ReleaseTest(unittest.TestCase):
+#    fixtures = ['test-data.json']
+    
+    def setUp(self):
+        pass
+    
+    def test_release(self):
+        """
+        Test relationships between releases and other elements.
+
+        >>> from django.core.management import call_command
+        >>> call_command('loaddata', 'projects/fixtures/test-data.json')
+        Installing json fixture 'projects/fixtures/test-data' from absolute path.
+        ...
+
+        Test the release -> project relationship
+        >>> p = Project.objects.get(slug='test') 
+        >>> r = p.releases.get(slug='test-release')                
+        
+        Test the release -> project relationship
+        >>> r = Release.objects.get(slug='test-release')
+        >>> r.project
+        <Project: Test Project>
+        """

transifex/projects/urls.py

 from projects.views.project import *
 from projects.views.component import *
 from projects.views.permission import *
+from projects.views.release import *
 
 from txcommon.decorators import one_perm_required_or_403
 from webtrans.wizards import TransFormWizard
         view = project_toggle_watch,
         name = 'project_toggle_watch',),
 )
+      
 
 urlpatterns += patterns('django.views.generic',
     url(
              template_object_name='project'),
         name='project_tag_list'),
 )
+      
 
 
 # Components
         view = component_detail,
         name = 'component_detail'),
 )
+      
+
+# Releases
+urlpatterns += patterns('',
+    url(
+        regex = '^p/(?P<project_slug>[-\w]+)/r/(?P<release_slug>[-\w]+)/$',
+        view = release_detail,
+        name = 'release_detail'),
+    url(
+        regex = '^p/(?P<project_slug>[-\w]+)/add-release/$',
+        view = release_create_update,
+        name = 'release_create',),
+    url(
+        regex = '^p/(?P<project_slug>[-\w]+)/c/(?P<release_slug>[-\w]+)/edit/$',
+        view = release_create_update,
+        name = 'release_edit',),
+    url(
+        regex = '^p/(?P<project_slug>[-\w]+)/r/(?P<release_slug>[-\w]+)/delete/$',
+        view = release_delete,
+        name = 'release_delete',),
+)
+      
 
 #TODO: Make this setting work throughout the applications
 if getattr(settings, 'ENABLE_WEBTRANS', True):

transifex/projects/views/release.py

+# -*- coding: utf-8 -*-
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+
+from actionlog.models import action_logging
+from projects.models import Project, Release
+from projects.forms import ReleaseForm
+from projects.permissions import (pr_release_add_change, pr_release_delete)
+
+# Temporary
+from txcommon import notifications as txnotification
+from txcommon.decorators import one_perm_required_or_403
+from txcommon.log import logger
+
+
+##############################################
+# Releases
+
+def release_detail(request, project_slug, release_slug):
+    release = get_object_or_404(Release, slug=release_slug,
+                                project__slug=project_slug)
+    return render_to_response('projects/release_details.html', {
+        'release': release,
+        'project': release.project,
+    }, context_instance=RequestContext(request))
+
+
+@login_required
+@one_perm_required_or_403(pr_release_add_change,
+    (Project, 'slug__exact', 'project_slug'))
+def release_create_update(request, project_slug, release_slug=None, *args, **kwargs):
+    project = get_object_or_404(Project, slug__exact=project_slug)
+    if release_slug:
+        release = get_object_or_404(Release, slug=release_slug,
+                                    project__slug=project_slug)
+    else:
+        release = None
+    if request.method == 'POST':
+        release_form = ReleaseForm(project, request.POST, instance=release)
+        if release_form.is_valid():
+            release = release_form.save()
+
+            return HttpResponseRedirect(
+                reverse('release_detail',
+                         args=[project_slug, release.slug]))
+    else:
+        release_form = ReleaseForm(project, instance=release)
+
+    return render_to_response('projects/release_form.html', {
+        'form': release_form,
+        'project': project,
+        'release': release,
+    }, context_instance=RequestContext(request))
+
+
+@login_required
+@one_perm_required_or_403(pr_release_delete, 
+    (Project, 'slug__exact', 'project_slug'))
+def release_delete(request, project_slug, release_slug):
+    release = get_object_or_404(Release, slug=release_slug,
+                                project__slug=project_slug)
+    if request.method == 'POST':
+        import copy
+        release_ = copy.copy(release)
+        release.delete()
+        request.user.message_set.create(
+            message=_("%s was deleted.") % release.full_name)
+
+        # ActionLog & Notification
+        nt = 'project_release_deleted'
+        context = {'release': release_}
+        action_logging(request.user, [release_.project], nt, context=context)
+        if settings.ENABLE_NOTICES:
+            txnotification.send_observation_notices_for(release_.project,
+                                signal=nt, extra_context=context)
+
+        return HttpResponseRedirect(reverse('project_detail', 
+                                     args=(project_slug,)))
+    else:
+        return render_to_response('projects/release_confirm_delete.html',
+                                  {'release': release,},
+                                  context_instance=RequestContext(request))
+

transifex/templates/base-sample.html

     </div>
     <div id="navmenu">
       <a href="{% url project_list %}" title="{% trans "The projects being served" %}">{% trans "Projects" %}</a>
-      | <a href="{% url collection_list %}" title="{% trans "Collections of projects" %}">{% trans "Collections" %}</a>
       | <a href="{% url language_list %}" title="{% trans "The languages our service serves" %}">{% trans "Languages" %}</a>
     </div>
     <div id="header_secnav">

transifex/templates/projects/component_detail.html

     <tr>
       <th class="i16 release">{% blocktrans count releases|length as counter %}Release:{% plural %}Releases:{% endblocktrans %}</th>
       <td class="compact">
-        {% for release in releases|slice:"0:6" %}<a class="release" href="{% url collection_release_detail slug=release.collection.slug release_slug=release.slug %}">{{ release }}</a> {% endfor %}
+        {% for release in releases|slice:"0:6" %}<a class="release" href="{% url release_detail slug=release.project.slug release_slug=release.slug %}">{{ release }}</a> {% endfor %}
       </td>
     </tr>
     {% endif %}

transifex/templates/projects/project_detail.html

   
   <div class="details separate">
 
-  {% if project.homepage or project.tags or project.collections.all %}
+  {% if project.homepage or project.tags %}
     <h4>{% blocktrans %}Details{% endblocktrans %}</h4>
   {% endif %}
   <table class="definition">
       </td>
     </tr>
     {% endif %}
-    {% with project.collections.all as colls %}
-    {% if colls %}
-    <tr>
-      <th class="i16 collection">{% blocktrans count colls|length as counter %}Collection:{% plural %}Collections:{% endblocktrans %}</th>
-      <td class="compact">
-        {% for coll in colls|slice:"0:6" %}<a class="collection" href="{% url collection_detail slug=coll.slug %}">{{ coll }}</a> {% endfor %}
-      </td>
-    </tr>
-    {% endif %}
-    {% endwith %}
     {% with project.maintainers.all as maintainers %}
     {% if maintainers %}
     <tr>
 
 {% include "projects/component_list.html" %}
 
+{% include "projects/release_list.html" %}
+
 </div>
 
 <h3>{% trans 'History' %}</h3>

transifex/templates/projects/release_confirm_delete.html

+{% extends "projects/project_detail_childs.html" %}
+{% load i18n %}
+
+{% block title %}{% with release.project as project %}
+  {{ block.super }}
+  | {% blocktrans with release.name as release_name %}Delete {{ release_name }}{% endblocktrans %}
+{% endwith %}{% endblock %}
+
+{% block breadcrumb %}{% with release.project as project %}{{ block.super }}
+&raquo; {% blocktrans with release.name as release_name %}Delete {{ release_name }}{% endblocktrans %}
+{% endwith %}{% endblock %}
+
+{% block content_title %}
+  <h2 class="pagetitle">{% blocktrans with release.name as release_name %}Say goodbye to <em>{{ release_name }}</em>?{% endblocktrans %}</h2>
+{% endblock %}
+
+{% block content %}
+
+<p>{% blocktrans with release.project.name as project_name and release.name as release_name %}
+  Are you sure you want to permanently delete the release '{{ release_name }}' which belongs to {{ project_name }}?
+{% endblocktrans %}</p>
+
+  <form action='' method='post'>
+    <input type="submit" class="i16 submit" value="{% trans "Yes, I'm sure!" %}" />
+  </form>
+
+{% endblock %}
+
+{% block content_footer %}
+
+{% endblock %}

transifex/templates/projects/release_details.html

+{% extends "projects/project_detail_childs.html" %}
+{% load statistics %}
+{% load markup %}
+{% load i18n %}
+{% load truncate %}
+{% load permissions %}
+{% load txpermissions %}
+
+{% block body_class %}{{ block.super }} release_detail{% endblock %}
+
+{% block title %}{{ block.super }} | {{ release.name }}{% endblock %}
+
+{% block breadcrumb %}{{ block.super }} &raquo; {{ release.name }}{% endblock %}
+
+{% block content_main %}
+  <div class="obj_bigdetails">
+  <h2 class="name">{{ release.project.name }} &raquo; {{ release }}</h2>
+  
+  {% if release.description %}<p class="description">{{ release.description }}</p>{% endif %}
+
+  {% with release.long_description_html as long_desc %}
+  {% if long_desc %}
+  <div class="long_description">
+    {{ long_desc|truncatewords:"100"|safe }}
+  </div>
+  {% endif %}
+  {% endwith %}
+
+{% comment %}
+  {% if request.user.is_authenticated and perms.projects.change_release %}
+    <div class="editlinks">
+      <p><a class="i16 edit buttonized" href="{% url project_release_edit slug=release.project.slug release_slug=release.slug %}">{% trans "Edit" %}</a></p>
+    </div>
+  {% endif %}
+{% endcomment %}
+
+  <div class="details">
+
+  {% if release.homepage or release.release_date %}
+  <h4>{% blocktrans %}Details{% endblocktrans %}</h4>
+  {% endif %}
+  <table class="definition">
+    <tr {% if not release.homepage %}class="empty {% if not request.user.is_authenticated %}nodisplay{% endif %}"{% endif %}>
+      <th class="homepage i16">{% trans "Homepage:" %}</th>
+      <td><a href="{{ release.homepage }}">{{ release.homepage }}</a></td>
+    </tr>
+    <tr class="separator"></tr>
+    <tr class="{% if not release.release_date %}empty {% if not request.user.is_authenticated %}nodisplay{% endif %}{% endif %}">
+      <th class="release_date i16">{% trans "Release date:" %}</th>
+      <td>{{ release.release_date|date:"D d M Y" }}</td>
+    </tr>
+    <tr {% if not release.stringfreeze_date %}class="empty {% if not request.user.is_authenticated %}nodisplay{% endif %}"{% endif %}>
+      <th class="freeze i16">{% trans "String freeze:" %}</th>
+      <td>{{ release.stringfreeze_date|date:"D d M Y" }}</td>
+    </tr>
+    <tr {% if not release.develfreeze_date %}class="empty {% if not request.user.is_authenticated %}nodisplay{% endif %}"{% endif %}>
+      <th class="freeze i16">{% trans "Devel freeze:" %}</th>
+      <td>{{ release.develfreeze_date|date:"D d M Y" }}</td>
+    </tr>
+  </table>
+</div>
+
+<div id="projects" class="projects">
+
+<h3>{% trans "Translation statistics" %}</h3>
+
+{% include "translations/stats_table_filter_box.html" %}
+
+{% if not pofile_list %}
+<p>{% trans "No translations added for this release. :-(" %}</p>
+{% endif %}
+
+</div>
+</div>
+{% endblock %}{# body_main #}
+
+{% block content_footer %}
+  <div id="content_footer_center">
+    {% if request.user.is_authenticated and perms.projects.delete_release %}
+    <div class="deletelink">
+      <a class="i16 delete buttonized" href="{% url release_delete release.project.slug release.slug %}">{% trans "Delete release" %}</a>
+    </div>
+    {% endif %}
+  </div>
+{% endblock %}

transifex/templates/projects/release_form.html

+{% extends "projects/base.html" %}
+{% load i18n %}
+{% load txcommontags %}
+
+{% block extra_head %}
+  <script type="text/javascript" src="/admin/jsi18n/"></script>
+  <script type="text/javascript" src="/media/admin/js/core.js"></script>
+ {{ form.media }}
+{% endblock %}
+
+{% block title %}
+  {% if not release %}{{ block.super }} | {% trans "Add a release" %}
+  {% else %}{{ block.super }} | {% blocktrans with release.name as release_name %}Edit {{ release_name}}{% endblocktrans %}{% endif %}
+{% endblock %}
+
+{% block breadcrumb %}
+  {% if not release %}{{ block.super }} &raquo; <a href="{{ project.get_absolute_url }}">{{ project }}</a> &raquo; {% trans "Add a release" %}
+  {% else %}{{ block.super }} &raquo; <a href="{{ project.get_absolute_url }}">{{ project }}</a> &raquo; {% blocktrans with release.name as release_name %}Edit <em>{{ release_name}}</em> release{% endblocktrans %}{% endif %}
+{% endblock %}
+
+{% block content_title %}
+  {% if not release %}
+    <h2 class="pagetitle">{% trans "Add a release" %}</h2>
+  {% else %}
+    <h2 class="pagetitle">
+      {% blocktrans with release.name as release_name and release.get_absolute_url as release_url %}
+        Editing <a href="{{ release_url }}">{{ release_name }}</a>
+      {% endblocktrans %}
+    </h2>
+  {% endif %}
+{% endblock %}
+
+{% block content %}
+  <div class="release_create generic_form">
+  {% if form_message %}
+    <p class="message i16 bell">{{ form_message }}</p>
+  {% endif %}
+  <form action='' method='post'>
+  <table>
+  <tbody>
+    {% form_as_table_rows form %}
+  </tbody>
+  </table>
+  <p class="submit"><input type="submit" class="i16 submit" value="{% trans "Save release" %}" /></p>
+  </form>
+  </div>
+{% endblock %}

transifex/templates/projects/release_list.html

+{% load txcommontags %}
+{% load i18n %}
+{% url release_create project_slug=project.slug as release_create %}
+<div id="releases" class="releases separate">
+  <div class="editlinks">
+    {# <p><a class="simlink" href="{% url collection_feed param=collection.slug %}" title="collection feed"><img border="0" src="{{ MEDIA_URL }}images/icons/feed.png" /></a></p> #}
+      {% if request.user.is_authenticated and perms.txcollections.add_collectionrelease %}
+        <p><a class="i16 add buttonized" href="{{ release_create }}">{% trans "Add" %}</a></p>
+      {% endif %}
+    </p>
+  </div>
+
+  {% with project.releases.all as releases %}
+  <h3>{% blocktrans %}Project Releases{% endblocktrans %}
+  {% render_metacount releases _("releases") %}
+  </h3>
+
+  {% if releases %}
+  <div class="release_list">
+  <ul class="simple bigthings">
+  {% for release in releases %}
+    <li><p class="i16 release"><a href="{{ release.get_absolute_url }}">{{ release.name }}</a>
+    {% if release.description %}<span class="description">– {{ release.description }}</span>{% endif %}</p></li>
+  {% endfor %}
+  </ul>
+  </div>
+  {% else %}
+    {% if request.user.is_authenticated and perms.txcollections.add_collectionrelease %}
+      <p class="i16 tip">{% blocktrans %}No releases are registered for this project yet. Why don't you <a href="{{ release_create }}">add one</a>?{% endblocktrans %}</p>
+    {% endif %}
+  {% endif %}
+  {% endwith %}
+</div>

transifex/translations/templatetags/statistics.py

             'collection': collection,
             'release': release}
 
+@register.inclusion_tag("release_stats_table.html")
+def release_stats_table(stats, project, release):
+    """
+    Create a HTML table to presents the statistics of all languages 
+    for a specific release.
+    """
+    return {'stats': key_sort(stats, 'language.name', '-trans_perc'),
+            'project': project,
+            'release': release}
+
 @register.inclusion_tag("comp_lang_stats_table.html", takes_context=True)
 def comp_lang_stats_table(context, stats):
     """

transifex/txcommon/notifications.py

                 "default": 2,
                 "show_to_user": False,
             },
+            {
+                "label": "project_release_added",
+                "display": _("New Release Added for a Watched Project"), 
+                "description": _("when a new release is added to a project"), 
+                "default": 2,
+                "show_to_user": False,
+            },
+            {
+                "label": "project_release_changed",
+                "display": _("Release Changed"), 
+                "description" :_("when a release of a project is changed"), 
+                "default": 1,
+                "show_to_user": False,
+            },
+            {   "label": "project_release_deleted",
+                "display": _("Release Deleted"), 
+                "description": _("when a release of a project is deleted"), 
+                "default": 1,
+                "show_to_user": False,
+            },
+            {   "label": "project_release_added",
+                "display": _("Release Added to a Project"), 
+                "description": _("when a release is added to a project"), 
+                "default": 2,
+                "show_to_user": False,
+            },
+            {   "label": "project_release_deleted",
+                "display": _("Release Deleted from a Project"), 
+                "description": _("when a release is deleted from a project"), 
+                "default": 2,
+                "show_to_user": False,
+            },
     ]