diff --git a/colab/__init__.py b/colab/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/__init__.py
diff --git a/colab/accounts/__init__.py b/colab/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/accounts/__init__.py
diff --git a/colab/accounts/admin.py b/colab/accounts/admin.py
new file mode 100644
index 0000000..c12ce74
--- /dev/null
+++ b/colab/accounts/admin.py
@@ -0,0 +1,56 @@
+
+from django import forms
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin
+from django.utils.translation import ugettext_lazy as _
+
+from .models import User
+
+
+class UserCreationForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ('username', 'email')
+
+ def __init__(self, *args, **kwargs):
+ super(UserCreationForm, self).__init__(*args, **kwargs)
+ self.fields['email'].required = True
+
+
+class UserChangeForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ('username', 'first_name', 'last_name', 'email', 'is_active',
+ 'is_staff', 'is_superuser', 'groups', 'last_login',
+ 'date_joined', 'twitter', 'facebook', 'google_talk',
+ 'webpage')
+
+ def __init__(self, *args, **kwargs):
+ super(UserChangeForm, self).__init__(*args, **kwargs)
+ self.fields['email'].required = True
+
+
+
+class MyUserAdmin(UserAdmin):
+ form = UserChangeForm
+ add_form = UserCreationForm
+
+ fieldsets = (
+ (None, {'fields': ('username', 'email')}),
+ (_('Personal info'), {'fields': ('first_name', 'last_name', 'twitter',
+ 'facebook', 'google_talk', 'webpage',
+ )}),
+ (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
+ 'groups')}),
+ (_('Important dates'), {'fields': ('last_login', 'date_joined')})
+ )
+
+ add_fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': ('username', 'email')}
+ ),
+ )
+
+
+admin.site.register(User, MyUserAdmin)
diff --git a/colab/accounts/auth.py b/colab/accounts/auth.py
new file mode 100644
index 0000000..b04fa78
--- /dev/null
+++ b/colab/accounts/auth.py
@@ -0,0 +1,6 @@
+
+from django_browserid.auth import BrowserIDBackend
+
+class ColabBrowserIDBackend(BrowserIDBackend):
+ def filter_users_by_email(self, email):
+ return self.User.objects.filter(emails__address=email)
diff --git a/colab/accounts/errors.py b/colab/accounts/errors.py
new file mode 100644
index 0000000..43d056b
--- /dev/null
+++ b/colab/accounts/errors.py
@@ -0,0 +1,2 @@
+class XMPPChangePwdException(Exception):
+ """Error changing XMPP Account password"""
diff --git a/colab/accounts/forms.py b/colab/accounts/forms.py
new file mode 100644
index 0000000..f55d13a
--- /dev/null
+++ b/colab/accounts/forms.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+from django import forms
+from django.contrib.auth import get_user_model
+from django.utils.translation import ugettext_lazy as _
+
+from conversejs.models import XMPPAccount
+
+from accounts.utils import mailman
+from super_archives.models import MailingList
+from .utils.validators import validate_social_account
+
+User = get_user_model()
+
+
+class SocialAccountField(forms.Field):
+ def __init__(self, *args, **kwargs):
+ self.url = kwargs.pop('url', None)
+ super(SocialAccountField, self).__init__(*args, **kwargs)
+
+ def validate(self, value):
+ super(SocialAccountField, self).validate(value)
+
+ if value and not validate_social_account(value, self.url):
+ raise forms.ValidationError(_('Social account does not exist'),
+ code='social-account-doesnot-exist')
+
+
+class UserForm(forms.ModelForm):
+ required = ('first_name', 'last_name', 'email', 'username')
+
+ class Meta:
+ model = User
+
+ def __init__(self, *args, **kwargs):
+ super(UserForm, self).__init__(*args, **kwargs)
+ for field_name, field in self.fields.items():
+ # Adds form-control class to all form fields
+ field.widget.attrs.update({'class': 'form-control'})
+
+ # Set UserForm.required fields as required
+ if field_name in UserForm.required:
+ field.required = True
+
+
+class UserCreationForm(UserForm):
+ class Meta:
+ model = User
+ fields = ('first_name', 'last_name', 'email', 'username')
+
+
+class UserUpdateForm(UserForm):
+ bio = forms.CharField(
+ widget=forms.Textarea(attrs={'rows': '6', 'maxlength': '200'}),
+ max_length=200,
+ label=_(u'Bio'),
+ help_text=_(u'Write something about you in 200 characters or less.'),
+ required=False,
+ )
+
+ class Meta:
+ model = User
+ fields = ('first_name', 'last_name',
+ 'institution', 'role', 'twitter', 'facebook',
+ 'google_talk', 'github', 'webpage', 'bio')
+
+ twitter = SocialAccountField(url='https://twitter.com/', required=False)
+ facebook = SocialAccountField(url='https://graph.facebook.com/', required=False)
+
+
+class ListsForm(forms.Form):
+ LISTS_NAMES = ((
+ listname, u'{} ({})'.format(listname, description)
+ ) for listname, description in mailman.all_lists(description=True))
+
+ lists = forms.MultipleChoiceField(label=_(u'Mailing lists'),
+ required=False,
+ widget=forms.CheckboxSelectMultiple,
+ choices=LISTS_NAMES)
+
+
+class ChangeXMPPPasswordForm(forms.ModelForm):
+ password1 = forms.CharField(label=_("Password"),
+ widget=forms.PasswordInput)
+ password2 = forms.CharField(label=_("Password confirmation"),
+ widget=forms.PasswordInput,
+ help_text=_("Enter the same password as above, for verification."))
+
+ class Meta:
+ model = XMPPAccount
+ fields = ('password1', 'password2')
+
+ def __init__(self, *args, **kwargs):
+ super(ChangeXMPPPasswordForm, self).__init__(*args, **kwargs)
+
+ for field_name, field in self.fields.items():
+ # Adds form-control class to all form fields
+ field.widget.attrs.update({'class': 'form-control'})
+
+ def clean_password2(self):
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ if password1 and password2 and password1 != password2:
+ raise forms.ValidationError(
+ _("Password mismatch"),
+ code='password_mismatch',
+ )
+ return password2
+
+ def save(self, commit=True):
+ self.instance.password = self.cleaned_data['password2']
+ if commit:
+ self.instance.save()
+ return self.instance
diff --git a/colab/accounts/management/__init__.py b/colab/accounts/management/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/accounts/management/__init__.py
diff --git a/colab/accounts/management/commands/__init__.py b/colab/accounts/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/accounts/management/commands/__init__.py
diff --git a/colab/accounts/management/commands/delete_invalid.py b/colab/accounts/management/commands/delete_invalid.py
new file mode 100644
index 0000000..0335e98
--- /dev/null
+++ b/colab/accounts/management/commands/delete_invalid.py
@@ -0,0 +1,42 @@
+
+
+from django.db.models import F
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from django.core.management.base import BaseCommand, CommandError
+
+
+from ...models import User
+
+
+class Command(BaseCommand):
+ """Delete user accounts that have never logged in.
+
+ Delete from database user accounts that have never logged in
+ and are at least 24h older.
+
+ """
+
+ help = __doc__
+
+ def handle(self, *args, **kwargs):
+ seconds = timezone.timedelta(seconds=1)
+ now = timezone.now()
+ one_day_ago = timezone.timedelta(days=1)
+
+ # Query for users that have NEVER logged in
+ #
+ # By default django sets the last_login as auto_now and then
+ # last_login is pretty much the same than date_joined
+ # (instead of null as I expected). Because of that we query
+ # for users which last_login is between date_joined - N and
+ # date_joined + N, where N is a small constant in seconds.
+ users = User.objects.filter(last_login__gt=(F('date_joined') - seconds),
+ last_login__lt=(F('date_joined') + seconds),
+ date_joined__lt=now-one_day_ago)
+ count = 0
+ for user in users:
+ count += 1
+ user.delete()
+
+ print _(u'%(count)s users deleted.') % {'count': count}
diff --git a/colab/accounts/migrations/0001_initial.py b/colab/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..59fd7a6
--- /dev/null
+++ b/colab/accounts/migrations/0001_initial.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.utils.timezone
+import django.core.validators
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('auth', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and ./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])),
+ ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
+ ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
+ ('email', models.EmailField(unique=True, max_length=75, verbose_name='email address', blank=True)),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ('institution', models.CharField(max_length=128, null=True, blank=True)),
+ ('role', models.CharField(max_length=128, null=True, blank=True)),
+ ('twitter', models.CharField(max_length=128, null=True, blank=True)),
+ ('facebook', models.CharField(max_length=128, null=True, blank=True)),
+ ('google_talk', models.EmailField(max_length=75, null=True, blank=True)),
+ ('github', models.CharField(max_length=128, null=True, verbose_name='github', blank=True)),
+ ('webpage', models.CharField(max_length=256, null=True, blank=True)),
+ ('verification_hash', models.CharField(max_length=32, null=True, blank=True)),
+ ('modified', models.DateTimeField(auto_now=True)),
+ ('bio', models.CharField(max_length=200, null=True, blank=True)),
+ ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')),
+ ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
+ ],
+ options={
+ 'abstract': False,
+ 'verbose_name': 'user',
+ 'verbose_name_plural': 'users',
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/colab/accounts/migrations/__init__.py b/colab/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/accounts/migrations/__init__.py
diff --git a/colab/accounts/models.py b/colab/accounts/models.py
new file mode 100644
index 0000000..e530afa
--- /dev/null
+++ b/colab/accounts/models.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+import urlparse
+
+from django.db import models, DatabaseError
+from django.contrib.auth.hashers import check_password
+from django.contrib.auth.models import AbstractUser
+from django.core import validators
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from conversejs import xmpp
+
+from .utils import mailman
+
+
+class User(AbstractUser):
+ institution = models.CharField(max_length=128, null=True, blank=True)
+ role = models.CharField(max_length=128, null=True, blank=True)
+ twitter = models.CharField(max_length=128, null=True, blank=True)
+ facebook = models.CharField(max_length=128, null=True, blank=True)
+ google_talk = models.EmailField(null=True, blank=True)
+ github = models.CharField(max_length=128, null=True, blank=True,
+ verbose_name=u'github')
+ webpage = models.CharField(max_length=256, null=True, blank=True)
+ verification_hash = models.CharField(max_length=32, null=True, blank=True)
+ modified = models.DateTimeField(auto_now=True)
+ bio = models.CharField(max_length=200, null=True, blank=True)
+
+ def check_password(self, raw_password):
+
+ if self.xmpp.exists() and raw_password == self.xmpp.first().password:
+ return True
+
+ return super(User, self).check_password(raw_password)
+
+ def get_absolute_url(self):
+ return reverse('user_profile', kwargs={'username': self.username})
+
+ def twitter_link(self):
+ return urlparse.urljoin('https://twitter.com', self.twitter)
+
+ def facebook_link(self):
+ return urlparse.urljoin('https://www.facebook.com', self.facebook)
+
+ def mailinglists(self):
+ return mailman.user_lists(self)
+
+ def update_subscription(self, email, lists):
+ mailman.update_subscription(email, lists)
+
+
+# We need to have `email` field set as unique but Django does not
+# support field overriding (at least not until 1.6).
+# The following workaroud allows to change email field to unique
+# without having to rewrite all AbstractUser here
+User._meta.get_field('email')._unique = True
+User._meta.get_field('username').help_text = _(
+ u'Required. 30 characters or fewer. Letters, digits and '
+ u'./+/-/_ only.'
+)
+User._meta.get_field('username').validators[0] = validators.RegexValidator(
+ r'^[\w.+-]+$',
+ _('Enter a valid username.'),
+ 'invalid'
+)
diff --git a/colab/accounts/search_indexes.py b/colab/accounts/search_indexes.py
new file mode 100644
index 0000000..ec13877
--- /dev/null
+++ b/colab/accounts/search_indexes.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+
+from haystack import indexes
+from django.db.models import Count
+
+from badger.utils import get_users_counters
+from .models import User
+
+
+class UserIndex(indexes.SearchIndex, indexes.Indexable):
+ # common fields
+ text = indexes.CharField(document=True, use_template=True, stored=False)
+ url = indexes.CharField(model_attr='get_absolute_url', indexed=False)
+ title = indexes.CharField(model_attr='get_full_name')
+ description = indexes.CharField(null=True)
+ type = indexes.CharField()
+ icon_name = indexes.CharField()
+
+ # extra fields
+ username = indexes.CharField(model_attr='username', stored=False)
+ name = indexes.CharField(model_attr='get_full_name')
+ email = indexes.CharField(model_attr='email', stored=False)
+ institution = indexes.CharField(model_attr='institution', null=True)
+ role = indexes.CharField(model_attr='role', null=True)
+ google_talk = indexes.CharField(model_attr='google_talk', null=True,
+ stored=False)
+ webpage = indexes.CharField(model_attr='webpage', null=True, stored=False)
+ message_count = indexes.IntegerField(stored=False)
+ changeset_count = indexes.IntegerField(stored=False)
+ ticket_count = indexes.IntegerField(stored=False)
+ wiki_count = indexes.IntegerField(stored=False)
+ contribution_count = indexes.IntegerField(stored=False)
+
+ def get_model(self):
+ return User
+
+ @property
+ def badge_counters(self):
+ if not hasattr(self, '_badge_counters'):
+ self._badge_counters = get_users_counters()
+ return self._badge_counters
+
+ def prepare(self, obj):
+ prepared_data = super(UserIndex, self).prepare(obj)
+
+ prepared_data['contribution_count'] = sum((
+ self.prepared_data['message_count'],
+ self.prepared_data['changeset_count'],
+ self.prepared_data['ticket_count'],
+ self.prepared_data['wiki_count']
+ ))
+
+ return prepared_data
+
+ def prepare_description(self, obj):
+ return u'{}\n{}\n{}\n{}'.format(
+ obj.institution, obj.role, obj.username, obj.get_full_name()
+ )
+
+ def prepare_icon_name(self, obj):
+ return u'user'
+
+ def prepare_type(self, obj):
+ return u'user'
+
+ def prepare_message_count(self, obj):
+ return self.badge_counters[obj.username]['messages']
+
+ def prepare_changeset_count(self, obj):
+ return self.badge_counters[obj.username]['revisions']
+
+ def prepare_ticket_count(self, obj):
+ return self.badge_counters[obj.username]['tickets']
+
+ def prepare_wiki_count(self, obj):
+ return self.badge_counters[obj.username]['wikis']
+
+ def index_queryset(self, using=None):
+ return self.get_model().objects.filter(is_active=True)
diff --git a/colab/accounts/templates/accounts/change_password.html b/colab/accounts/templates/accounts/change_password.html
new file mode 100644
index 0000000..221148d
--- /dev/null
+++ b/colab/accounts/templates/accounts/change_password.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block main-content %}
+
+{% endblock %}
diff --git a/colab/accounts/templates/accounts/manage_subscriptions.html b/colab/accounts/templates/accounts/manage_subscriptions.html
new file mode 100644
index 0000000..a4908d0
--- /dev/null
+++ b/colab/accounts/templates/accounts/manage_subscriptions.html
@@ -0,0 +1,45 @@
+{% extends 'base.html' %}
+{% load i18n gravatar %}
+
+{% block main-content %}
+
+ {% blocktrans %}Group Subscriptions{% endblocktrans %}
+ {% gravatar user_.email 50 %} {{ user_.get_full_name }} ({{ user_.username }})
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/colab/accounts/templates/accounts/user_create_form.html b/colab/accounts/templates/accounts/user_create_form.html
new file mode 100644
index 0000000..2b7bb38
--- /dev/null
+++ b/colab/accounts/templates/accounts/user_create_form.html
@@ -0,0 +1,66 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% block main-content %}
+
+{% trans "Sign up" %}
+
+
+ {% if form.errors %}
+
+ {% trans "Please correct the errors below and try again" %}
+
+ {% endif %}
+
+
+
+
+ {% trans "Required fields" %}
+
+
+
+
+{% endblock %}
diff --git a/colab/accounts/templates/accounts/user_detail.html b/colab/accounts/templates/accounts/user_detail.html
new file mode 100644
index 0000000..eac8758
--- /dev/null
+++ b/colab/accounts/templates/accounts/user_detail.html
@@ -0,0 +1,182 @@
+{% extends "base.html" %}
+
+{% load i18n gravatar i18n_model %}
+
+{% block title %}Perfil{% endblock %}
+
+{% block head_js %}
+ {% trans "Messages" as group_collabs %}
+ {% trans "Contributions" as type_collabs %}
+
+ {% include "doughnut-chart.html" with chart_data=type_count chart_canvas="collabs" name=type_collabs %}
+ {% include "doughnut-chart.html" with chart_data=list_activity chart_canvas="collabs2" name=group_collabs %}
+{% endblock %}
+
+{% block main-content %}
+
+
+
+
+ {% gravatar user_.email 200 %}
+
+
+
+ {{ user_.get_full_name }}
+ {{ user_.username }}
+
+
+ {% if request.user == user_ or request.user.is_superuser %}
+
{% trans "edit profile"|title %}
+
{% trans "group membership"|title %}
+ {% endif %}
+
+ {% if request.user.is_active %}
+ {% if user_.bio %}
+
+
+
+ {% trans 'Bio' %}
+
+ {{ user_.bio }}
+
+ {% endif %}
+ {% endif %}
+
+
+ {% if request.user.is_active %}
+
+
+ {% endif %}
+
+
+ {% if user_.institution or user_.role %}
+
+
+ {{ user_.role }}
+ {% if user_.institution and user_.role %}-{% endif %}
+ {{ user_.institution }}
+
+ {% endif %}
+ {% if request.user.is_active %}
+
+ {% if user_.twitter %}
+ {{ user_.twitter }}
+ {% endif %}
+ {% if user_.facebook %}
+ {{ user_.facebook }}
+ {% endif %}
+
+
+ {% if user_.google_talk %}
+ {{ user_.google_talk }}
+ {% endif %}
+
+ {% if user_.github %}
+ {{ user_.github }}
+ {% endif %}
+
+ {% if user_.webpage %}
+ {{ user_.webpage }}
+ {% endif %}
+ {% endif %}
+
+
+ {% if user_.mailinglists %}
+
{% trans 'Groups: ' %}
+ {% for list in user_.mailinglists %}
+
{{ list }}
+ {% endfor %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
{% trans "Collaborations by Type" %}
+
+
+
+
+
+
+
+
+
+
{% trans "Participation by Group" %}
+
+
+
+
+
+
+ {% if user_.badge_set.exists %}
+
+
+
+
{% trans "Badges" %}
+
+
+
+ {% for badge in user_.badge_set.all %}
+ {% translate badge as badge_trans %}
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
{% trans "Latest posted" %}
+
+ {% for doc in emails %}
+ {% include "message-preview.html" with result=doc %}
+ {% empty %}
+ {% trans "There are no posts by this user so far." %}
+ {% endfor %}
+
+
+ {% trans "View more posts..." %}
+
+
+
+
+
+
{% trans "Latest contributions" %}
+
+ {% for result in results %}
+ {% include "message-preview.html" %}
+ {% empty %}
+ {% trans "No contributions of this user so far." %}
+ {% endfor %}
+
+
+ {% trans "View more contributions..." %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/colab/accounts/templates/accounts/user_update_form.html b/colab/accounts/templates/accounts/user_update_form.html
new file mode 100644
index 0000000..27304fb
--- /dev/null
+++ b/colab/accounts/templates/accounts/user_update_form.html
@@ -0,0 +1,207 @@
+{% extends "base.html" %}
+{% load i18n gravatar %}
+
+{% block head_js %}
+
+{% endblock %}
+
+
+{% block main-content %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/colab/accounts/templates/search/indexes/accounts/user_text.txt b/colab/accounts/templates/search/indexes/accounts/user_text.txt
new file mode 100644
index 0000000..35b480f
--- /dev/null
+++ b/colab/accounts/templates/search/indexes/accounts/user_text.txt
@@ -0,0 +1,7 @@
+{{ object.username }}
+{{ object.get_full_name }}
+{{ object.get_full_name|slugify }}
+{{ object.institution }}
+{{ object.institution|slugify }}
+{{ object.role }}
+{{ object.role|slugify }}
diff --git a/colab/accounts/templatetags/__init__.py b/colab/accounts/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/accounts/templatetags/__init__.py
diff --git a/colab/accounts/templatetags/gravatar.py b/colab/accounts/templatetags/gravatar.py
new file mode 100644
index 0000000..ceff4a8
--- /dev/null
+++ b/colab/accounts/templatetags/gravatar.py
@@ -0,0 +1,20 @@
+
+from django import template
+
+from super_archives.models import EmailAddress
+
+
+register = template.Library()
+
+
+@register.simple_tag
+def gravatar(email, size=80):
+ if isinstance(email, basestring):
+ try:
+ email = EmailAddress.objects.get(address=email)
+ except EmailAddress.DoesNotExist:
+ pass
+
+ email_md5 = getattr(email, 'md5', 'anonymous')
+
+ return u' '.format(email_md5, size, size, size)
diff --git a/colab/accounts/tests.py b/colab/accounts/tests.py
new file mode 100644
index 0000000..501deb7
--- /dev/null
+++ b/colab/accounts/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
diff --git a/colab/accounts/urls.py b/colab/accounts/urls.py
new file mode 100644
index 0000000..c9ca9f9
--- /dev/null
+++ b/colab/accounts/urls.py
@@ -0,0 +1,25 @@
+
+from django.conf.urls import patterns, include, url
+
+from .views import (UserProfileDetailView, UserProfileUpdateView,
+ ManageUserSubscriptionsView, ChangeXMPPPasswordView)
+
+from accounts import views
+
+urlpatterns = patterns('',
+ url(r'^register/$', 'accounts.views.signup', name='signup'),
+
+ url(r'^change-password/$',
+ ChangeXMPPPasswordView.as_view(), name='change_password'),
+
+ url(r'^logout/?$', 'accounts.views.logoutColab', name='logout'),
+
+ url(r'^(?P[\w@+.-]+)/?$',
+ UserProfileDetailView.as_view(), name='user_profile'),
+
+ url(r'^(?P[\w@+.-]+)/edit/?$',
+ UserProfileUpdateView.as_view(), name='user_profile_update'),
+
+ url(r'^(?P[\w@+.-]+)/subscriptions/?$',
+ ManageUserSubscriptionsView.as_view(), name='user_list_subscriptions'),
+)
diff --git a/colab/accounts/utils/__init__.py b/colab/accounts/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/accounts/utils/__init__.py
diff --git a/colab/accounts/utils/mailman.py b/colab/accounts/utils/mailman.py
new file mode 100644
index 0000000..07fe7ac
--- /dev/null
+++ b/colab/accounts/utils/mailman.py
@@ -0,0 +1,97 @@
+
+import urlparse
+import requests
+import logging
+
+from django.conf import settings
+
+TIMEOUT = 1
+
+
+def get_url(listname=None):
+ if listname:
+ return urlparse.urljoin(settings.MAILMAN_API_URL, '/' + listname)
+
+ return settings.MAILMAN_API_URL
+
+
+def subscribe(listname, address):
+ url = get_url(listname)
+ try:
+ requests.put(url, timeout=TIMEOUT, data={'address': address})
+ except:
+ logging.exception('Unable to subscribe user')
+ return False
+ return True
+
+
+def unsubscribe(listname, address):
+ url = get_url(listname)
+ try:
+ requests.delete(url, timeout=TIMEOUT, data={'address': address})
+ except:
+ logging.exception('Unable to unsubscribe user')
+ return False
+ return True
+
+
+def update_subscription(address, lists):
+ current_lists = address_lists(address)
+
+ for maillist in current_lists:
+ if maillist not in lists:
+ unsubscribe(maillist, address)
+
+ for maillist in lists:
+ if maillist not in current_lists:
+ subscribe(maillist, address)
+
+
+def address_lists(address, description=''):
+ url = get_url()
+
+ params = {'address': address,
+ 'description': description}
+
+ try:
+ lists = requests.get(url, timeout=TIMEOUT, params=params)
+ except:
+ logging.exception('Unable to list mailing lists')
+ return []
+
+ return lists.json()
+
+
+def all_lists(*args, **kwargs):
+ return address_lists('', *args, **kwargs)
+
+
+def user_lists(user):
+ list_set = set()
+
+ for email in user.emails.values_list('address', flat=True):
+ list_set.update(address_lists(email))
+
+ return tuple(list_set)
+
+
+def get_list_description(listname, lists=None):
+ if not lists:
+ lists = dict(all_lists(description=True))
+ elif not isinstance(lists, dict):
+ lists = dict(lists)
+
+ return lists.get(listname)
+
+
+def list_users(listname):
+ url = get_url(listname)
+
+ params = {}
+
+ try:
+ users = requests.get(url, timeout=TIMEOUT, params=params)
+ except requests.exceptions.RequestException:
+ return []
+
+ return users.json()
diff --git a/colab/accounts/utils/validators.py b/colab/accounts/utils/validators.py
new file mode 100644
index 0000000..68f4128
--- /dev/null
+++ b/colab/accounts/utils/validators.py
@@ -0,0 +1,26 @@
+
+import urllib2
+import urlparse
+
+
+def validate_social_account(account, url):
+ """Verifies if a social account is valid.
+
+ Examples:
+
+ >>> validate_social_account('seocam', 'http://twitter.com')
+ True
+
+ >>> validate_social_account('seocam-fake-should-fail', 'http://twitter.com')
+ False
+ """
+
+ request = urllib2.Request(urlparse.urljoin(url, account))
+ request.get_method = lambda: 'HEAD'
+
+ try:
+ response = urllib2.urlopen(request)
+ except urllib2.HTTPError:
+ return False
+
+ return response.code == 200
diff --git a/colab/accounts/views.py b/colab/accounts/views.py
new file mode 100644
index 0000000..e1ec516
--- /dev/null
+++ b/colab/accounts/views.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+import datetime
+
+from collections import OrderedDict
+
+from django.contrib.auth.views import logout
+from django.contrib import messages
+from django.db import transaction
+from django.db.models import Count
+from django.contrib.auth import get_user_model
+from django.utils.translation import ugettext as _
+from django.shortcuts import render, redirect, get_object_or_404
+from django.core.urlresolvers import reverse
+from django.core.exceptions import PermissionDenied
+from django.views.generic import DetailView, UpdateView
+from django.utils.decorators import method_decorator
+
+from django.http import HttpResponse
+from conversejs import xmpp
+from conversejs.models import XMPPAccount
+from haystack.query import SearchQuerySet
+
+from super_archives.models import EmailAddress, Message
+from search.utils import trans
+#from proxy.trac.models import WikiCollabCount, TicketCollabCount
+from .forms import (UserCreationForm, ListsForm, UserUpdateForm,
+ ChangeXMPPPasswordForm)
+from .errors import XMPPChangePwdException
+from .utils import mailman
+
+
+class UserProfileBaseMixin(object):
+ model = get_user_model()
+ slug_field = 'username'
+ slug_url_kwarg = 'username'
+ context_object_name = 'user_'
+
+
+class UserProfileUpdateView(UserProfileBaseMixin, UpdateView):
+ template_name = 'accounts/user_update_form.html'
+ form_class = UserUpdateForm
+
+ def get_success_url(self):
+ return reverse('user_profile', kwargs={'username': self.object.username})
+
+ def get_object(self, *args, **kwargs):
+ obj = super(UserProfileUpdateView, self).get_object(*args, **kwargs)
+ if self.request.user != obj and not self.request.user.is_superuser:
+ raise PermissionDenied
+
+ return obj
+
+
+class UserProfileDetailView(UserProfileBaseMixin, DetailView):
+ template_name = 'accounts/user_detail.html'
+
+ def get_context_data(self, **kwargs):
+ user = self.object
+ context = {}
+
+ count_types = OrderedDict()
+
+ fields_or_lookup = (
+ {'collaborators__contains': user.username},
+ {'fullname_and_username__contains': user.username},
+ )
+
+ counter_class = {}
+ #{
+ # 'wiki': WikiCollabCount,
+ # 'ticket': TicketCollabCount,
+ #}
+
+ types = ['thread']
+ #types.extend(['ticket', 'wiki', 'changeset', 'attachment'])
+
+ messages = Message.objects.filter(from_address__user__pk=user.pk)
+ for type in types:
+ CounterClass = counter_class.get(type)
+ if CounterClass:
+ try:
+ counter = CounterClass.objects.get(author=user.username)
+ except CounterClass.DoesNotExist:
+ count_types[trans(type)] = 0
+ else:
+ count_types[trans(type)] = counter.count
+ elif type == 'thread':
+ count_types[trans(type)] = messages.count()
+ else:
+ sqs = SearchQuerySet()
+ for filter_or in fields_or_lookup:
+ sqs = sqs.filter_or(type=type, **filter_or)
+ count_types[trans(type)] = sqs.count()
+
+ context['type_count'] = count_types
+
+ sqs = SearchQuerySet()
+ for filter_or in fields_or_lookup:
+ sqs = sqs.filter_or(**filter_or).exclude(type='thread')
+
+ context['results'] = sqs.order_by('-modified', '-created')[:10]
+
+ email_pks = [addr.pk for addr in user.emails.iterator()]
+ query = Message.objects.filter(from_address__in=email_pks)
+ query = query.order_by('-received_time')
+ context['emails'] = query[:10]
+
+ count_by = 'thread__mailinglist__name'
+ context['list_activity'] = dict(messages.values_list(count_by)\
+ .annotate(Count(count_by))\
+ .order_by(count_by))
+
+ context.update(kwargs)
+ return super(UserProfileDetailView, self).get_context_data(**context)
+
+
+def logoutColab(request):
+ response = logout(request, next_page='/')
+ response.delete_cookie('_redmine_session')
+ response.delete_cookie('_gitlab_session')
+ return response
+
+
+def signup(request):
+ # If the request method is GET just return the form
+ if request.method == 'GET':
+ user_form = UserCreationForm()
+ lists_form = ListsForm()
+ return render(request, 'accounts/user_create_form.html',
+ {'user_form': user_form, 'lists_form': lists_form})
+
+ user_form = UserCreationForm(request.POST)
+ lists_form = ListsForm(request.POST)
+
+ if not user_form.is_valid() or not lists_form.is_valid():
+ return render(request, 'accounts/user_create_form.html',
+ {'user_form': user_form, 'lists_form': lists_form})
+
+ user = user_form.save()
+
+ # Check if the user's email have been used previously
+ # in the mainling lists to link the user to old messages
+ email_addr, created = EmailAddress.objects.get_or_create(address=user.email)
+ if created:
+ email_addr.real_name = user.get_full_name()
+
+ email_addr.user = user
+ email_addr.save()
+
+ mailing_lists = lists_form.cleaned_data.get('lists')
+ mailman.update_subscription(user.email, mailing_lists)
+
+ messages.success(request, _('Your profile has been created!'))
+ messages.warning(request, _('You must login to validated your profile. '
+ 'Profiles not validated are deleted in 24h.'))
+
+ return redirect('user_profile', username=user.username)
+
+
+class ManageUserSubscriptionsView(UserProfileBaseMixin, DetailView):
+ http_method_names = [u'get', u'post']
+ template_name = u'accounts/manage_subscriptions.html'
+
+ def get_object(self, *args, **kwargs):
+ obj = super(ManageUserSubscriptionsView, self).get_object(*args,
+ **kwargs)
+ if self.request.user != obj and not self.request.user.is_superuser:
+ raise PermissionDenied
+
+ return obj
+
+ def post(self, request, *args, **kwargs):
+ user = self.get_object()
+ for email in user.emails.values_list('address', flat=True):
+ lists = self.request.POST.getlist(email)
+ user.update_subscription(email, lists)
+
+ return redirect('user_profile', username=user.username)
+
+ def get_context_data(self, **kwargs):
+ context = {}
+ context['membership'] = {}
+
+ user = self.get_object()
+ emails = user.emails.values_list('address', flat=True)
+ all_lists = mailman.all_lists(description=True)
+
+ for email in emails:
+ lists = []
+ lists_for_address = mailman.address_lists(email)
+ for listname, description in all_lists:
+ if listname in lists_for_address:
+ checked = True
+ else:
+ checked = False
+ lists.append((
+ {'listname': listname, 'description': description},
+ checked
+ ))
+
+ context['membership'].update({email: lists})
+
+ context.update(kwargs)
+
+ return super(ManageUserSubscriptionsView, self).get_context_data(**context)
+
+
+class ChangeXMPPPasswordView(UpdateView):
+ model = XMPPAccount
+ form_class = ChangeXMPPPasswordForm
+ fields = ['password', ]
+ template_name = 'accounts/change_password.html'
+
+ def get_success_url(self):
+ return reverse('user_profile', kwargs={
+ 'username': self.request.user.username
+ })
+
+ def get_object(self, queryset=None):
+ obj = get_object_or_404(XMPPAccount, user=self.request.user.pk)
+ self.old_password = obj.password
+ return obj
+
+ def form_valid(self, form):
+ transaction.set_autocommit(False)
+
+ response = super(ChangeXMPPPasswordView, self).form_valid(form)
+
+ changed = xmpp.change_password(
+ self.object.jid,
+ self.old_password,
+ form.cleaned_data['password1']
+ )
+
+ if not changed:
+ messages.error(
+ self.request,
+ _(u'Could not change your password. Please, try again later.')
+ )
+ transaction.rollback()
+ return response
+ else:
+ transaction.commit()
+
+ messages.success(
+ self.request,
+ _("You've changed your password successfully!")
+ )
+ return response
diff --git a/colab/api/__init__.py b/colab/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/api/__init__.py
diff --git a/colab/api/resources.py b/colab/api/resources.py
new file mode 100644
index 0000000..6a891ce
--- /dev/null
+++ b/colab/api/resources.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+
+from django.contrib.auth import get_user_model
+
+from tastypie import fields
+from tastypie.constants import ALL_WITH_RELATIONS, ALL
+from tastypie.resources import ModelResource
+
+from super_archives.models import Message, EmailAddress
+#from proxy.trac.models import Revision, Ticket, Wiki
+
+User = get_user_model()
+
+
+class UserResource(ModelResource):
+ class Meta:
+ queryset = User.objects.filter(is_active=True)
+ resource_name = 'user'
+ fields = ['username', 'institution', 'role', 'bio', 'first_name',
+ 'last_name', 'email']
+ allowed_methods = ['get', ]
+ filtering = {
+ 'email': ('exact', ),
+ 'username': ALL,
+ 'institution': ALL,
+ 'role': ALL,
+ 'bio': ALL,
+ }
+
+ def dehydrate_email(self, bundle):
+ return ''
+
+
+class EmailAddressResource(ModelResource):
+ user = fields.ForeignKey(UserResource, 'user', full=False, null=True)
+
+ class Meta:
+ queryset = EmailAddress.objects.all()
+ resource_name = 'emailaddress'
+ excludes = ['md5', ]
+ allowed_methods = ['get', ]
+ filtering = {
+ 'address': ('exact', ),
+ 'user': ALL_WITH_RELATIONS,
+ 'real_name': ALL,
+ }
+
+ def dehydrate_address(self, bundle):
+ return ''
+
+
+class MessageResource(ModelResource):
+ from_address = fields.ForeignKey(EmailAddressResource, 'from_address',
+ full=False)
+
+ class Meta:
+ queryset = Message.objects.all()
+ resource_name = 'message'
+ excludes = ['spam', 'subject_clean', 'message_id']
+ filtering = {
+ 'from_address': ALL_WITH_RELATIONS,
+ 'subject': ALL,
+ 'body': ALL,
+ 'received_time': ALL,
+ }
+
+
+#class RevisionResource(ModelResource):
+# class Meta:
+# queryset = Revision.objects.all()
+# resource_name = 'revision'
+# excludes = ['collaborators', ]
+# filtering = {
+# 'key': ALL,
+# 'rev': ALL,
+# 'author': ALL,
+# 'message': ALL,
+# 'repository_name': ALL,
+# 'created': ALL,
+# }
+#
+#
+#class TicketResource(ModelResource):
+# class Meta:
+# queryset = Ticket.objects.all()
+# resource_name = 'ticket'
+# excludes = ['collaborators', ]
+# filtering = {
+# 'id': ALL,
+# 'summary': ALL,
+# 'description': ALL,
+# 'milestone': ALL,
+# 'priority': ALL,
+# 'component': ALL,
+# 'version': ALL,
+# 'severity': ALL,
+# 'reporter': ALL,
+# 'author': ALL,
+# 'status': ALL,
+# 'keywords': ALL,
+# 'created': ALL,
+# 'modified': ALL,
+# 'modified_by': ALL,
+# }
+#
+#
+#class WikiResource(ModelResource):
+# class Meta:
+# queryset = Wiki.objects.all()
+# resource_name = 'wiki'
+# excludes = ['collaborators', ]
+# filtering = {
+# 'name': ALL,
+# 'wiki_text': ALL,
+# 'author': ALL,
+# 'name': ALL,
+# 'created': ALL,
+# 'modified': ALL,
+# 'modified_by': ALL,
+# }
diff --git a/colab/api/urls.py b/colab/api/urls.py
new file mode 100644
index 0000000..4e0b29c
--- /dev/null
+++ b/colab/api/urls.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+
+from django.conf.urls import patterns, include, url
+
+from tastypie.api import Api
+
+from .resources import (UserResource, EmailAddressResource, MessageResource)
+from .views import VoteView
+
+
+api = Api(api_name='v1')
+api.register(UserResource())
+api.register(EmailAddressResource())
+api.register(MessageResource())
+
+
+urlpatterns = patterns('',
+ url(r'message/(?P\d+)/vote$', VoteView.as_view()),
+
+ # tastypie urls
+ url(r'', include(api.urls)),
+)
diff --git a/colab/api/views.py b/colab/api/views.py
new file mode 100644
index 0000000..d92b4f8
--- /dev/null
+++ b/colab/api/views.py
@@ -0,0 +1,45 @@
+
+from django import http
+from django.db import IntegrityError
+from django.views.generic import View
+from django.core.exceptions import ObjectDoesNotExist
+
+
+from super_archives.models import Message
+
+
+class VoteView(View):
+
+ http_method_names = [u'get', u'put', u'delete', u'head']
+
+ def put(self, request, msg_id):
+ if not request.user.is_authenticated():
+ return http.HttpResponseForbidden()
+
+ try:
+ Message.objects.get(id=msg_id).vote(request.user)
+ except IntegrityError:
+ # 409 Conflict
+ # used for duplicated entries
+ return http.HttpResponse(status=409)
+
+ # 201 Created
+ return http.HttpResponse(status=201)
+
+ def get(self, request, msg_id):
+ votes = Message.objects.get(id=msg_id).votes_count()
+ return http.HttpResponse(votes, content_type='application/json')
+
+ def delete(self, request, msg_id):
+ if not request.user.is_authenticated():
+ return http.HttpResponseForbidden()
+
+ try:
+ Message.objects.get(id=msg_id).unvote(request.user)
+ except ObjectDoesNotExist:
+ return http.HttpResponseGone()
+
+ # 204 No Content
+ # empty body, as per RFC2616.
+ # object deleted
+ return http.HttpResponse(status=204)
diff --git a/colab/badger/__init__.py b/colab/badger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/badger/__init__.py
diff --git a/colab/badger/admin.py b/colab/badger/admin.py
new file mode 100644
index 0000000..c0d0b0d
--- /dev/null
+++ b/colab/badger/admin.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+from .forms import BadgeForm
+from .models import Badge, BadgeI18N
+
+
+class BadgeI18NInline(admin.TabularInline):
+ model = BadgeI18N
+
+
+class BadgeAdmin(admin.ModelAdmin):
+ form = BadgeForm
+ inlines = [BadgeI18NInline, ]
+ list_display = ['title', 'description', 'order']
+ list_editable = ['order', ]
+
+
+admin.site.register(Badge, BadgeAdmin)
diff --git a/colab/badger/forms.py b/colab/badger/forms.py
new file mode 100644
index 0000000..f5c6125
--- /dev/null
+++ b/colab/badger/forms.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+
+import base64
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from PIL import Image
+
+from .models import Badge
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+class BadgeForm(forms.ModelForm):
+ image = forms.ImageField(label=_(u'Image'), required=False)
+
+ class Meta:
+ model = Badge
+ fields = (
+ 'title', 'description', 'image', 'user_attr', 'comparison',
+ 'value', 'awardees'
+ )
+
+ def clean_image(self):
+ if not self.instance.pk and not self.cleaned_data['image']:
+ raise forms.ValidationError(_(u'You must add an Image'))
+ return self.cleaned_data['image']
+
+ def save(self, commit=True):
+
+ instance = super(BadgeForm, self).save(commit=False)
+
+ if self.cleaned_data['image']:
+ img = Image.open(self.cleaned_data['image'])
+ img = img.resize((50, 50), Image.ANTIALIAS)
+ f = StringIO()
+ img.save(f, 'png')
+ instance.image_base64 = f.getvalue().encode('base64')
+ f.close()
+
+ if commit:
+ instance.save()
+
+ return instance
diff --git a/colab/badger/management/__init__.py b/colab/badger/management/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/badger/management/__init__.py
diff --git a/colab/badger/management/commands/__init__.py b/colab/badger/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/badger/management/commands/__init__.py
diff --git a/colab/badger/management/commands/rebuild_badges.py b/colab/badger/management/commands/rebuild_badges.py
new file mode 100644
index 0000000..5cbb813
--- /dev/null
+++ b/colab/badger/management/commands/rebuild_badges.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+from django.core.management.base import BaseCommand, CommandError
+from haystack.query import SearchQuerySet
+
+from accounts.models import User
+from badger.models import Badge
+
+
+class Command(BaseCommand):
+ help = "Rebuild the user's badges."
+
+ def handle(self, *args, **kwargs):
+ for badge in Badge.objects.filter(type='auto'):
+ if not badge.comparison:
+ continue
+ elif badge.comparison == 'biggest':
+ order = u'-{}'.format(Badge.USER_ATTR_OPTS[badge.user_attr])
+ sqs = SearchQuerySet().filter(type='user')
+ user = sqs.order_by(order)[0]
+ badge.awardees.remove(*list(badge.awardees.all()))
+ badge.awardees.add(User.objects.get(pk=user.pk))
+ continue
+
+ comparison = u'__{}'.format(badge.comparison) if badge.comparison \
+ is not 'equal' else u''
+
+ key = u'{}{}'.format(
+ Badge.USER_ATTR_OPTS[badge.user_attr],
+ comparison
+ )
+ opts = {key: badge.value}
+
+ sqs = SearchQuerySet().filter(
+ type='user',
+ **opts
+ )
+
+ # Remove all awardees to make sure that all of then
+ # still accomplish the necessary to keep the badge
+ badge.awardees.remove(*list(badge.awardees.all()))
+
+ for user in sqs:
+ badge.awardees.add(User.objects.get(pk=user.pk))
diff --git a/colab/badger/management/commands/update_badges.py b/colab/badger/management/commands/update_badges.py
new file mode 100644
index 0000000..7c59a44
--- /dev/null
+++ b/colab/badger/management/commands/update_badges.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+from django.core.management.base import BaseCommand, CommandError
+from haystack.query import SearchQuerySet
+
+from accounts.models import User
+from badger.models import Badge
+
+import logging
+
+class Command(BaseCommand):
+ help = "Update the user's badges"
+
+ def update_badges(self):
+ for badge in Badge.objects.filter(type='auto'):
+ if not badge.comparison:
+ continue
+ elif badge.comparison == 'biggest':
+ order = u'-{}'.format(Badge.USER_ATTR_OPTS[badge.user_attr])
+ sqs = SearchQuerySet().filter(type='user')
+ user = sqs.order_by(order)[0]
+ badge.awardees.add(User.objects.get(pk=user.pk))
+ continue
+
+ comparison = u'__{}'.format(badge.comparison) if badge.comparison \
+ is not 'equal' else u''
+
+ key = u'{}{}'.format(
+ Badge.USER_ATTR_OPTS[badge.user_attr],
+ comparison
+ )
+ opts = {key: badge.value}
+
+ sqs = SearchQuerySet().filter(type='user', **opts)
+
+ for user in sqs:
+ badge.awardees.add(User.objects.get(pk=user.pk))
+
+ def handle(self, *args, **kwargs):
+ try:
+ self.update_badges()
+ except Exception as e:
+ logging.exception(e)
+ raise
diff --git a/colab/badger/migrations/0001_initial.py b/colab/badger/migrations/0001_initial.py
new file mode 100644
index 0000000..2909a1a
--- /dev/null
+++ b/colab/badger/migrations/0001_initial.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Badge',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('title', models.CharField(max_length=200, null=True, verbose_name='Title', blank=True)),
+ ('description', models.CharField(max_length=200, null=True, verbose_name='Description', blank=True)),
+ ('image_base64', models.TextField(verbose_name='Image')),
+ ('type', models.CharField(max_length=200, verbose_name='Type', choices=[('auto', 'Automatically'), ('manual', 'Manual')])),
+ ('user_attr', models.CharField(blank=True, max_length=100, null=True, verbose_name='User attribute', choices=[('messages', 'Messages'), ('contributions', 'Contributions'), ('wikis', 'Wikis'), ('revisions', 'Revisions'), ('tickets', 'Ticket')])),
+ ('comparison', models.CharField(blank=True, max_length=10, null=True, verbose_name='Comparison', choices=[('gte', 'Greater than or equal'), ('lte', 'less than or equal'), ('equal', 'Equal'), ('biggest', 'Biggest')])),
+ ('value', models.PositiveSmallIntegerField(null=True, verbose_name='Value', blank=True)),
+ ('order', models.PositiveSmallIntegerField(default=100, verbose_name='Order')),
+ ('awardees', models.ManyToManyField(to=settings.AUTH_USER_MODEL, null=True, verbose_name='Awardees', blank=True)),
+ ],
+ options={
+ 'ordering': ['order'],
+ 'verbose_name': 'Badge',
+ 'verbose_name_plural': 'Badges',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='BadgeI18N',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('i18n_language', models.CharField(max_length=10, verbose_name='language', choices=[(b'pt-br', 'Portuguese'), (b'es', 'Spanish')])),
+ ('title', models.CharField(max_length=200, null=True, verbose_name='Title', blank=True)),
+ ('description', models.CharField(max_length=200, null=True, verbose_name='Description', blank=True)),
+ ('i18n_source', models.ForeignKey(related_name=b'translations', editable=False, to='badger.Badge', verbose_name='source')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AlterUniqueTogether(
+ name='badgei18n',
+ unique_together=set([('i18n_source', 'i18n_language')]),
+ ),
+ ]
diff --git a/colab/badger/migrations/__init__.py b/colab/badger/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/badger/migrations/__init__.py
diff --git a/colab/badger/models.py b/colab/badger/models.py
new file mode 100644
index 0000000..b5b72bf
--- /dev/null
+++ b/colab/badger/models.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from i18n_model.models import I18nModel
+
+
+class Badge(models.Model):
+ COMPARISON_CHOICES = (
+ (u'gte', _(u'Greater than or equal')),
+ (u'lte', _(u'less than or equal')),
+ (u'equal', _(u'Equal')),
+ (u'biggest', _(u'Biggest')),
+ )
+ TYPE_CHOICES = (
+ (u'auto', _(u'Automatically')),
+ (u'manual', _(u'Manual')),
+ )
+ USER_ATTR_CHOICES = (
+ (u'messages', _(u'Messages')),
+ (u'contributions', _(u'Contributions')),
+ (u'wikis', _(u'Wikis')),
+ (u'revisions', _(u'Revisions')),
+ (u'tickets', _(u'Ticket')),
+ )
+ USER_ATTR_OPTS = {
+ u'messages': u'message_count',
+ u'revisions': u'changeset_count',
+ u'tickets': u'ticket_count',
+ u'wikis': u'wiki_count',
+ u'contributions': u'contribution_count',
+ }
+
+ title = models.CharField(_(u'Title'), max_length=200, blank=True,
+ null=True)
+ description = models.CharField(_(u'Description'), max_length=200,
+ blank=True, null=True)
+ image_base64 = models.TextField(_(u'Image'))
+ type = models.CharField(_(u'Type'), max_length=200, choices=TYPE_CHOICES)
+ user_attr = models.CharField(
+ _(u'User attribute'),max_length=100,
+ choices=USER_ATTR_CHOICES,
+ blank=True,
+ null=True,
+ )
+ comparison = models.CharField(
+ _(u'Comparison'),
+ max_length=10,
+ choices=COMPARISON_CHOICES,
+ blank=True,
+ null=True
+ )
+ value = models.PositiveSmallIntegerField(
+ _(u'Value'),
+ blank=True,
+ null=True
+ )
+ awardees = models.ManyToManyField(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_(u'Awardees'),
+ blank=True,
+ null=True
+ )
+ order = models.PositiveSmallIntegerField(_(u'Order'), default=100)
+
+ class Meta:
+ verbose_name = _(u'Badge')
+ verbose_name_plural = _(u'Badges')
+ ordering = ['order', ]
+
+ def __unicode__(self):
+ return u'{} ({}, {})'.format(
+ self.title,
+ self.get_user_attr_display(),
+ self.get_type_display(),
+ )
+
+
+class BadgeI18N(I18nModel):
+ class Meta:
+ source_model = Badge
+ translation_fields = ('title', 'description')
diff --git a/colab/badger/tests.py b/colab/badger/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/badger/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/badger/utils.py b/colab/badger/utils.py
new file mode 100644
index 0000000..cee5db9
--- /dev/null
+++ b/colab/badger/utils.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+from django.db.models import Count
+
+#from proxy.trac.models import (Revision, Ticket, Wiki,
+# WikiCollabCount, TicketCollabCount)
+from accounts.models import User
+
+
+def get_wiki_counters():
+ return {author: count for author, count in
+ WikiCollabCount.objects.values_list()}
+
+
+def get_revision_counters():
+ return {
+ author: count for author, count in Revision.objects.values_list(
+ 'author'
+ ).annotate(count=Count('author'))
+ }
+
+
+def get_ticket_counters():
+ return {author: count for author, count in
+ TicketCollabCount.objects.values_list()}
+
+
+def get_users_counters():
+ wiki_counters = get_wiki_counters()
+ revision_counters = get_revision_counters()
+ ticket_counters = get_ticket_counters()
+
+ users_counters = {}
+ for user in User.objects.annotate(message_count=Count('emails__message')):
+ users_counters[user.username] = {
+ 'messages': user.message_count,
+ 'wikis': wiki_counters.get(user.username, 0),
+ 'revisions': revision_counters.get(user.username, 0),
+ 'tickets': ticket_counters.get(user.username, 0),
+ }
+ return users_counters
diff --git a/colab/badger/views.py b/colab/badger/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/colab/badger/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/colab/colab/__init__.py b/colab/colab/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/colab/__init__.py
diff --git a/colab/colab/colab.template.yaml b/colab/colab/colab.template.yaml
new file mode 100644
index 0000000..13dbaaa
--- /dev/null
+++ b/colab/colab/colab.template.yaml
@@ -0,0 +1,53 @@
+
+DEBUG: false
+TEMPLATE_DEBUG: false
+
+ADMINS: &admin
+ -
+ - John Foo
+ - john@example.com
+ -
+ - Mary Bar
+ - mary@example.com
+
+MANAGERS: *admin
+
+COLAB_FROM_ADDRESS: '"Colab" '
+SERVER_EMAIL: '"Colab" '
+
+EMAIL_HOST: localhost
+EMAIL_PORT: 25
+EMAIL_SUBJECT_PREFIX: '[colab]'
+
+SECRET_KEY: '{{ secret_key }}'
+
+SITE_URL: 'http://www.example.com/'
+
+ALLOWED_HOSTS:
+ - example.com
+ - example.org
+ - example.net
+
+CONVERSEJS_ENABLED: false
+
+CONVERSEJS_AUTO_REGISTER: 'xmpp.example.com'
+
+DATABASES:
+ default:
+ ENGINE: django.db.backends.postgresql_psycopg2
+ HOST: localhost
+ NAME: colab
+ USER: colab
+ PASSWORD: colab
+
+ROBOTS_NOINDEX: false
+
+# Set to false to disable
+RAVEN_DSN: 'http://public:secret@example.com/1'
+
+PROXIED_APPS:
+ gitlab:
+ upstream: 'http://localhost:8090/gitlab/'
+ trac:
+ upstream: 'http://localhost:5000/trac/'
+
diff --git a/colab/colab/settings.py b/colab/colab/settings.py
new file mode 100644
index 0000000..ff14541
--- /dev/null
+++ b/colab/colab/settings.py
@@ -0,0 +1,315 @@
+"""
+Django settings for colab project.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.7/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.7/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+BASE_DIR = os.path.dirname(os.path.dirname(__file__))
+
+# Used for settings translation
+from django.utils.translation import ugettext_lazy as _
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "{{ secret_key }}"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+TEMPLATE_DEBUG = True
+
+ALLOWED_HOSTS = []
+
+DATABASE_ROUTERS = []
+
+# Application definition
+
+INSTALLED_APPS = (
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ # First app to provide AUTH_USER_MODEL to others
+ 'accounts',
+
+ # Not standard apps
+ 'raven.contrib.django.raven_compat',
+ 'cliauth',
+ 'django_mobile',
+ 'django_browserid',
+ 'conversejs',
+ 'haystack',
+ 'hitcounter',
+ 'i18n_model',
+ 'mptt',
+ 'dpaste',
+
+ # Own apps
+ 'super_archives',
+ 'api',
+ 'rss',
+ 'planet',
+ 'search',
+ 'badger',
+ 'tz',
+
+ # Feedzilla and deps
+ 'feedzilla',
+ 'taggit',
+ 'common',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'colab.urls'
+
+WSGI_APPLICATION = 'colab.wsgi.application'
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.7/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.7/howto/static-files/
+
+STATIC_ROOT = '/usr/share/nginx/colab/static/'
+MEDIA_ROOT = '/usr/share/nginx/colab/media/'
+
+STATIC_URL = '/static/'
+MEDIA_URL = '/media/'
+
+
+# Normally you should not import ANYTHING from Django directly
+# into your settings, but ImproperlyConfigured is an exception.
+from django.core.exceptions import ImproperlyConfigured
+
+
+def get_env_setting(setting):
+ """ Get the environment setting or return exception """
+ try:
+ return os.environ[setting]
+ except KeyError:
+ error_msg = "Set the %s env variable" % setting
+ raise ImproperlyConfigured(error_msg)
+
+
+# Allow Django runserver to serve SVG files
+# https://code.djangoproject.com/ticket/20162
+import mimetypes
+mimetypes.add_type('image/svg+xml', '.svg')
+
+LANGUAGES = (
+ ('en', _('English')),
+ ('pt-br', _('Portuguese')),
+ ('es', _('Spanish')),
+)
+
+DJANGO_DATE_FORMAT_TO_JS = {
+ 'pt-br': ('pt-BR', 'dd/MM/yyyy'),
+ 'es': ('es', 'dd/MM/yyyy'),
+}
+
+LANGUAGE_CODE = 'en'
+
+# The absolute path to the folder containing the attachments
+ATTACHMENTS_FOLDER_PATH = '/mnt/trac/attachments/'
+
+# ORDERING_DATA receives the options to order for as it's keys and a dict as
+# value, if you want to order for the last name, you can use something like:
+# 'last_name': {'name': 'Last Name', 'fields': 'last_name'} inside the dict,
+# you pass two major keys (name, fields)
+# The major key name is the name to appear on the template
+# the major key fields it show receive the name of the fields to order for in
+# the indexes
+
+ORDERING_DATA = {
+ 'latest': {
+ 'name': _(u'Recent activity'),
+ 'fields': ('-modified', '-created'),
+ },
+ 'hottest': {
+ 'name': _(u'Relevance'),
+ 'fields': None,
+ },
+}
+
+
+# File type groupings is a tuple of tuples containg what it should filter,
+# how it should be displayed, and a tuple of which mimetypes it includes
+FILE_TYPE_GROUPINGS = (
+ ('document', _(u'Document'),
+ ('doc', 'docx', 'odt', 'otx', 'dotx', 'pdf', 'ott')),
+ ('presentation', _(u'Presentation'), ('ppt', 'pptx', 'odp')),
+ ('text', _(u'Text'), ('txt', 'po', 'conf', 'log')),
+ ('code', _(u'Code'),
+ ('py', 'php', 'js', 'sql', 'sh', 'patch', 'diff', 'html', '')),
+ ('compressed', _(u'Compressed'), ('rar', 'zip', 'gz', 'tgz', 'bz2')),
+ ('image', _(u'Image'),
+ ('jpg', 'jpeg', 'png', 'tiff', 'gif', 'svg', 'psd', 'planner', 'cdr')),
+ ('spreadsheet', _(u'Spreadsheet'),
+ ('ods', 'xls', 'xlsx', 'xslt', 'csv')),
+)
+
+# the following variable define how many characters should be shown before
+# a highlighted word, to make sure that the highlighted word will appear
+HIGHLIGHT_NUM_CHARS_BEFORE_MATCH = 30
+HAYSTACK_CUSTOM_HIGHLIGHTER = 'colab.utils.highlighting.ColabHighlighter'
+
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'haystack.backends.solr_backend.SolrEngine',
+ 'URL': 'http://localhost:8983/solr/',
+ }
+}
+
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ 'LOCATION': '127.0.0.1:11211',
+ }
+}
+
+DATABASE_ROUTERS = []
+
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+ 'django.contrib.auth.context_processors.auth',
+ 'django.core.context_processors.debug',
+ 'django.core.context_processors.i18n',
+ 'django.core.context_processors.media',
+ 'django.core.context_processors.static',
+ 'django.core.context_processors.tz',
+ 'django.contrib.messages.context_processors.messages',
+ 'django.core.context_processors.request',
+ 'django_mobile.context_processors.is_mobile',
+ 'super_archives.context_processors.mailarchive',
+ 'proxy.context_processors.proxied_apps',
+ 'home.context_processors.robots',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django_mobile.middleware.MobileDetectionMiddleware',
+ 'django_mobile.middleware.SetFlavourMiddleware',
+ 'tz.middleware.TimezoneMiddleware',
+)
+
+# Add the django_browserid authentication backend.
+AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.ModelBackend',
+ 'accounts.auth.ColabBrowserIDBackend',
+)
+
+STATICFILES_DIRS = (
+ os.path.join(BASE_DIR, 'static'),
+)
+
+TEMPLATE_DIRS = (
+ os.path.join(BASE_DIR, 'templates'),
+)
+
+LOCALE_PATHS = (
+ os.path.join(BASE_DIR, 'locale'),
+)
+
+AUTH_USER_MODEL = 'accounts.User'
+
+from django.contrib.messages import constants as messages
+MESSAGE_TAGS = {
+ messages.INFO: 'alert-info',
+ messages.SUCCESS: 'alert-success',
+ messages.WARNING: 'alert-warning',
+ messages.ERROR: 'alert-danger',
+}
+
+### Feedzilla (planet)
+from feedzilla.settings import *
+FEEDZILLA_PAGE_SIZE = 5
+FEEDZILLA_SITE_TITLE = _(u'Planet Colab')
+FEEDZILLA_SITE_DESCRIPTION = _(u'Colab blog aggregator')
+
+### Mailman API settings
+MAILMAN_API_URL = 'http://localhost:9000'
+
+### BrowserID / Persona
+SITE_URL = 'localhost:8000'
+BROWSERID_AUDIENCES = [SITE_URL, SITE_URL.replace('https', 'http')]
+
+
+LOGIN_URL = '/'
+LOGIN_REDIRECT_URL = '/'
+LOGIN_REDIRECT_URL_FAILURE = '/'
+LOGOUT_REDIRECT_URL = '/user/logout'
+BROWSERID_CREATE_USER = False
+
+REVPROXY_ADD_REMOTE_USER = True
+
+## Converse.js settings
+# This URL must use SSL in order to keep chat sessions secure
+CONVERSEJS_BOSH_SERVICE_URL = SITE_URL + '/http-bind'
+
+CONVERSEJS_ALLOW_CONTACT_REQUESTS = False
+CONVERSEJS_SHOW_ONLY_ONLINE_USERS = True
+
+
+# Tastypie settings
+TASTYPIE_DEFAULT_FORMATS = ['json', ]
+
+# Dpaste settings
+DPASTE_EXPIRE_CHOICES = (
+ ('onetime', _(u'One Time Snippet')),
+ (3600, _(u'In one hour')),
+ (3600 * 24 * 7, _(u'In one week')),
+ (3600 * 24 * 30, _(u'In one month')),
+ ('never', _(u'Never')),
+)
+DPASTE_EXPIRE_DEFAULT = DPASTE_EXPIRE_CHOICES[4][0]
+DPASTE_DEFAULT_GIST_DESCRIPTION = 'Gist created from Colab DPaste'
+DPASTE_DEFAULT_GIST_NAME = 'colab_paste'
+DPASTE_LEXER_DEFAULT = 'text'
+
+from .utils.conf import load_yaml_settings
+locals().update(load_yaml_settings())
+
+if locals().get('RAVEN_DSN', False):
+ RAVEN_CONFIG = {
+ 'dsn': RAVEN_DSN + '?timeout=30',
+ }
+
+for app_label in locals().get('PROXIED_APPS', {}).keys():
+ INSTALLED_APPS += ('proxy.{}'.format(app_label),)
diff --git a/colab/colab/urls.py b/colab/colab/urls.py
new file mode 100644
index 0000000..6d175c6
--- /dev/null
+++ b/colab/colab/urls.py
@@ -0,0 +1,45 @@
+from django.conf.urls import patterns, include, url, static
+from django.conf import settings
+from django.views.generic import TemplateView
+from django.contrib import admin
+
+from accounts.models import User
+from search.forms import ColabSearchForm
+from super_archives.models import Message
+
+
+admin.autodiscover()
+
+urlpatterns = patterns('',
+ url(r'^$', 'home.views.index', name='home'),
+ url(r'^robots.txt$', 'home.views.robots', name='robots'),
+
+ url(r'^open-data/$', TemplateView.as_view(template_name='open-data.html'),
+ name='opendata'),
+
+ url(r'^search/', include('search.urls')),
+ url(r'^archives/', include('super_archives.urls')),
+ url(r'^api/', include('api.urls')),
+ url(r'^rss/', include('rss.urls')),
+
+ url(r'^user/', include('accounts.urls')), # Kept for backwards compatibility
+ url(r'^signup/', include('accounts.urls')), # (same here) TODO: move to nginx
+ url(r'^account/', include('accounts.urls')),
+
+ url(r'', include('django_browserid.urls')),
+
+ url(r'^planet/', include('feedzilla.urls')),
+
+ url(r'paste/', include('dpaste.urls.dpaste')),
+
+ # Uncomment the next line to enable the admin:
+ url(r'^colab/admin/', include(admin.site.urls)),
+
+ url(r'^trac/', include('proxy.trac.urls')),
+)
+
+if settings.DEBUG:
+ urlpatterns += static.static(
+ settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT
+ )
diff --git a/colab/colab/utils/__init__.py b/colab/colab/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/colab/utils/__init__.py
diff --git a/colab/colab/utils/conf.py b/colab/colab/utils/conf.py
new file mode 100644
index 0000000..b73904a
--- /dev/null
+++ b/colab/colab/utils/conf.py
@@ -0,0 +1,32 @@
+
+import os
+import yaml
+
+from django.core.exceptions import ImproperlyConfigured
+
+
+class InaccessibleYAMLSettings(ImproperlyConfigured):
+ """Settings YAML is Inaccessible.
+
+ Check if the file exists and if you have read permissions."""
+
+
+def load_yaml_settings():
+ yaml_path = os.getenv('COLAB_SETTINGS', '/etc/colab.yaml')
+
+ if not os.path.exists(yaml_path):
+ msg = "The yaml file {} does not exist".format(yaml_path)
+ raise InaccessibleYAMLSettings(msg)
+
+ try:
+ with open(yaml_path) as yaml_file:
+ yaml_settings = yaml.load(yaml_file.read())
+ except IOError:
+ msg = ('Could not open settings file {}. Please '
+ 'check if the file exists and if user '
+ 'has read rights.').format(yaml_path)
+ raise InaccessibleYAMLSettings(msg)
+
+ return yaml_settings
+
+yaml_settings = load_yaml_settings()
diff --git a/colab/colab/utils/highlighting.py b/colab/colab/utils/highlighting.py
new file mode 100644
index 0000000..8f41b7c
--- /dev/null
+++ b/colab/colab/utils/highlighting.py
@@ -0,0 +1,38 @@
+from haystack.utils import Highlighter
+from django.conf import settings
+from django.utils.html import escape, strip_tags
+
+
+class ColabHighlighter(Highlighter):
+ def highlight(self, text_block):
+ self.text_block = escape(strip_tags(text_block))
+ highlight_locations = self.find_highlightable_words()
+ start_offset, end_offset = self.find_window(highlight_locations)
+ return self.render_html(highlight_locations, start_offset, end_offset)
+
+ def find_window(self, highlight_locations):
+ """Getting the HIGHLIGHT_NUM_CHARS_BEFORE_MATCH setting
+ to find how many characters before the first word found should
+ be removed from the window
+ """
+
+ if len(self.text_block) <= self.max_length:
+ return (0, self.max_length)
+
+ num_chars_before = getattr(
+ settings,
+ 'HIGHLIGHT_NUM_CHARS_BEFORE_MATCH',
+ 0
+ )
+
+ best_start, best_end = super(ColabHighlighter, self).find_window(
+ highlight_locations
+ )
+ if best_start <= num_chars_before:
+ best_end -= best_start
+ best_start = 0
+ else:
+ best_start -= num_chars_before
+ best_end -= num_chars_before
+
+ return (best_start, best_end)
diff --git a/colab/colab/wsgi.py b/colab/colab/wsgi.py
new file mode 100644
index 0000000..02679ab
--- /dev/null
+++ b/colab/colab/wsgi.py
@@ -0,0 +1,14 @@
+"""
+WSGI config for colab project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/
+"""
+
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "colab.settings")
+
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
diff --git a/colab/home/__init__.py b/colab/home/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/home/__init__.py
diff --git a/colab/home/admin.py b/colab/home/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/colab/home/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/colab/home/context_processors.py b/colab/home/context_processors.py
new file mode 100644
index 0000000..7cdb531
--- /dev/null
+++ b/colab/home/context_processors.py
@@ -0,0 +1,4 @@
+from django.conf import settings
+
+def robots(request):
+ return {'ROBOTS_NOINDEX': getattr(settings, 'ROBOTS_NOINDEX', False)}
diff --git a/colab/home/models.py b/colab/home/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/colab/home/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/colab/home/tests.py b/colab/home/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/home/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/home/views.py b/colab/home/views.py
new file mode 100644
index 0000000..607f756
--- /dev/null
+++ b/colab/home/views.py
@@ -0,0 +1,65 @@
+
+from collections import OrderedDict
+
+from django.conf import settings
+from django.core.cache import cache
+from django.shortcuts import render
+from django.http import HttpResponse, Http404
+
+from search.utils import trans
+from haystack.query import SearchQuerySet
+
+#from proxy.trac.models import WikiCollabCount, TicketCollabCount
+from super_archives.models import Thread
+
+
+def index(request):
+ """Index page view"""
+
+
+ latest_threads = Thread.objects.all()[:6]
+ hottest_threads = Thread.highest_score.from_haystack()[:6]
+
+ count_types = cache.get('home_chart')
+ if count_types is None:
+ count_types = OrderedDict()
+ count_types['thread'] = SearchQuerySet().filter(
+ type='thread',
+ ).count()
+ # TODO: this section should be inside trac app and only use it here
+ #if settings.TRAC_ENABLED:
+ # for type in ['changeset', 'attachment']:
+ # count_types[type] = SearchQuerySet().filter(
+ # type=type,
+ # ).count()
+
+ # count_types['ticket'] = sum([
+ # ticket.count for ticket in TicketCollabCount.objects.all()
+ # ])
+
+ # count_types['wiki'] = sum([
+ # wiki.count for wiki in WikiCollabCount.objects.all()
+ # ])
+
+ cache.set('home_chart', count_types)
+
+ for key in count_types.keys():
+ count_types[trans(key)] = count_types.pop(key)
+
+ context = {
+ 'hottest_threads': hottest_threads[:6],
+ 'latest_threads': latest_threads,
+ 'type_count': count_types,
+ 'latest_results': SearchQuerySet().all().order_by(
+ '-modified', '-created'
+ )[:6],
+ }
+ return render(request, 'home.html', context)
+
+
+def robots(request):
+ if getattr(settings, 'ROBOTS_NOINDEX', False):
+ return HttpResponse('User-agent: *\nDisallow: /',
+ content_type='text/plain')
+
+ raise Http404
diff --git a/colab/locale/pt_BR/LC_MESSAGES/django.mo b/colab/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..ade8834
Binary files /dev/null and b/colab/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/colab/locale/pt_BR/LC_MESSAGES/django.po b/colab/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..0070732
--- /dev/null
+++ b/colab/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,1540 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-08-07 12:49+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: accounts/admin.py:40
+msgid "Personal info"
+msgstr "Informações Pessoais"
+
+#: accounts/admin.py:43
+msgid "Permissions"
+msgstr "Permissões"
+
+#: accounts/admin.py:45
+msgid "Important dates"
+msgstr "Datas importantes"
+
+#: accounts/forms.py:25
+msgid "Social account does not exist"
+msgstr "Conta social não existe"
+
+#: accounts/forms.py:56 accounts/templates/accounts/user_detail.html:38
+msgid "Bio"
+msgstr "Bio"
+
+#: accounts/forms.py:57
+msgid "Write something about you in 200 characters or less."
+msgstr "Escreva algo sobre você em 200 caracteres ou menos."
+
+#: accounts/forms.py:76
+msgid "Mailing lists"
+msgstr "Listas de e-mail"
+
+#: accounts/forms.py:83
+msgid "Password"
+msgstr "Senha"
+
+#: accounts/forms.py:85
+msgid "Password confirmation"
+msgstr "Confirmação de senha"
+
+#: accounts/forms.py:87
+msgid "Enter the same password as above, for verification."
+msgstr "Digite a mesma senha que acima, para verificação."
+
+#: accounts/forms.py:105
+msgid "Password mismatch"
+msgstr "Senhas diferentes"
+
+#: accounts/models.py:59
+msgid "Required. 30 characters or fewer. Letters, digits and ./+/-/_ only."
+msgstr ""
+"Obrigatório. 30 caracteres ou menos. Letras, números e ./+/-/_ somente."
+
+#: accounts/models.py:64
+msgid "Enter a valid username."
+msgstr "Insira um nome de usuário válido."
+
+#: accounts/views.py:144
+msgid "Your profile has been created!"
+msgstr "Seu perfil foi criado!"
+
+#: accounts/views.py:145
+msgid ""
+"You must login to validated your profile. Profiles not validated are deleted "
+"in 24h."
+msgstr ""
+"Você deve se logar para validar seu perfil. Perfis não validados serão "
+"deletados em 24h."
+
+#: accounts/views.py:229
+msgid "Could not change your password. Please, try again later."
+msgstr ""
+"Não conseguimos alterar sua senha. Por favor, tente novamente mais tarde."
+
+#: accounts/views.py:238
+msgid "You've changed your password successfully!"
+msgstr "Senha alterada com sucesso!"
+
+#: accounts/management/commands/delete_invalid.py:42
+#, python-format
+msgid "%(count)s users deleted."
+msgstr "%(count)s usuários deletados."
+
+#: accounts/templates/accounts/change_password.html:8
+msgid "Change XMPP Client and SVN Password"
+msgstr "Trocar senha do Repositório e do Mensageiro"
+
+#: accounts/templates/accounts/change_password.html:17
+#: accounts/templates/accounts/user_update_form.html:195
+msgid "Change Password"
+msgstr "Trocar senha"
+
+#: accounts/templates/accounts/manage_subscriptions.html:6
+msgid "Group Subscriptions"
+msgstr "Inscrições em grupos"
+
+#: accounts/templates/accounts/manage_subscriptions.html:36
+msgid "Update subscriptions"
+msgstr "Atualizar inscrições"
+
+#: accounts/templates/accounts/user_create_form.html:5
+msgid "Sign up"
+msgstr "Cadastrar"
+
+#: accounts/templates/accounts/user_create_form.html:10
+msgid "Please correct the errors below and try again"
+msgstr "Por favor, corrija os erros abaixo e tente novamente"
+
+#: accounts/templates/accounts/user_create_form.html:17
+msgid "Required fields"
+msgstr "Campos obrigatórios"
+
+#: accounts/templates/accounts/user_create_form.html:29
+msgid "Personal Information"
+msgstr "Informações pessoais"
+
+#: accounts/templates/accounts/user_create_form.html:46
+msgid "Subscribe to groups"
+msgstr "Inscreva-se nos grupos"
+
+#: accounts/templates/accounts/user_create_form.html:60
+#: templates/base.html:106 templates/base.html.py:111
+msgid "Register"
+msgstr "Cadastre-se"
+
+#: accounts/templates/accounts/user_detail.html:8 badger/models.py:22
+#: super_archives/models.py:258
+msgid "Messages"
+msgstr "Mensagens"
+
+#: accounts/templates/accounts/user_detail.html:9 badger/models.py:23
+#: templates/home.html:7
+msgid "Contributions"
+msgstr "Contribuições"
+
+#: accounts/templates/accounts/user_detail.html:29
+msgid "edit profile"
+msgstr "editar perfil"
+
+#: accounts/templates/accounts/user_detail.html:30
+msgid "group membership"
+msgstr "Inscrições nos grupos"
+
+#: accounts/templates/accounts/user_detail.html:65
+msgid "Twitter account"
+msgstr "Conta Twitter"
+
+#: accounts/templates/accounts/user_detail.html:68
+msgid "Facebook account"
+msgstr "Conta Facebook"
+
+#: accounts/templates/accounts/user_detail.html:73
+msgid "Google talk account"
+msgstr "Conta Google"
+
+#: accounts/templates/accounts/user_detail.html:77
+msgid "Github account"
+msgstr "Conta Github"
+
+#: accounts/templates/accounts/user_detail.html:81
+msgid "Personal webpage"
+msgstr "Página web pessoal"
+
+#: accounts/templates/accounts/user_detail.html:87
+msgid "Groups: "
+msgstr "Grupos: "
+
+#: accounts/templates/accounts/user_detail.html:100
+msgid "Collaborations by Type"
+msgstr "Colaborações por tipo"
+
+#: accounts/templates/accounts/user_detail.html:116
+msgid "Participation by Group"
+msgstr "Participação por grupo"
+
+#: accounts/templates/accounts/user_detail.html:132 badger/models.py:70
+msgid "Badges"
+msgstr "Medalhas"
+
+#: accounts/templates/accounts/user_detail.html:151
+msgid "Latest posted"
+msgstr "Últimas postagens"
+
+#: accounts/templates/accounts/user_detail.html:156
+msgid "There are no posts by this user so far."
+msgstr "Não há posts deste usuário até agora."
+
+#: accounts/templates/accounts/user_detail.html:160
+msgid "View more posts..."
+msgstr "Ver mais postagens..."
+
+#: accounts/templates/accounts/user_detail.html:166
+msgid "Latest contributions"
+msgstr "Últimas colaborações"
+
+#: accounts/templates/accounts/user_detail.html:171
+msgid "No contributions of this user so far."
+msgstr "Não há posts deste usuário até agora."
+
+#: accounts/templates/accounts/user_detail.html:175
+msgid "View more contributions..."
+msgstr "Ver mais colaborações..."
+
+#: accounts/templates/accounts/user_update_form.html:65
+msgid "We sent a verification email to "
+msgstr "Enviamos um email de verificação para "
+
+#: accounts/templates/accounts/user_update_form.html:66
+msgid "Please follow the instructions in it."
+msgstr "Por favor, siga as instruções."
+
+#: accounts/templates/accounts/user_update_form.html:110
+msgid "profile information"
+msgstr "informações do perfil"
+
+#: accounts/templates/accounts/user_update_form.html:115
+msgid "Change your avatar at Gravatar.com"
+msgstr "Troque seu avatar em Gravatar.com"
+
+#: accounts/templates/accounts/user_update_form.html:142 search/utils.py:8
+msgid "Emails"
+msgstr "E-mails"
+
+#: accounts/templates/accounts/user_update_form.html:151
+msgid "Primary"
+msgstr "Primário"
+
+#: accounts/templates/accounts/user_update_form.html:154
+msgid "Setting..."
+msgstr "Definindo..."
+
+#: accounts/templates/accounts/user_update_form.html:154
+msgid "Set as Primary"
+msgstr "Definir como Primário"
+
+#: accounts/templates/accounts/user_update_form.html:155
+msgid "Deleting..."
+msgstr "Deletando..."
+
+#: accounts/templates/accounts/user_update_form.html:155
+#: accounts/templates/accounts/user_update_form.html:167
+msgid "Delete"
+msgstr "Apagar"
+
+#: accounts/templates/accounts/user_update_form.html:166
+msgid "Sending verification..."
+msgstr "Enviando verificação..."
+
+#: accounts/templates/accounts/user_update_form.html:166
+msgid "Verify"
+msgstr "Verificar"
+
+#: accounts/templates/accounts/user_update_form.html:174
+msgid "Add another email address:"
+msgstr "Adicionar outro endereço de e-mail"
+
+#: accounts/templates/accounts/user_update_form.html:177
+msgid "Add"
+msgstr "Adicionar"
+
+#: accounts/templates/accounts/user_update_form.html:193
+msgid ""
+"This feature is available only for those who need to change the password for some "
+"reason as having an old user with the same username, forgot your password to "
+"commit, usage of other XMPP Client for connection. Usually, you won't need "
+"to change this password. Only change it if you are sure about what you are "
+"doing."
+msgstr ""
+"Este recurso está disponível para quem precisa trocar a senha por algum "
+"motivo como ter um usuário antigo com mesmo nome de usuário, esqueceu da "
+"senha antiga para commit, uso de outro cliente XMPP para conexão. "
+"Normalmente, você não terá que trocar essa senha. Somente troque essa senha "
+"se tiver certeza do que está fazendo."
+
+#: accounts/templates/accounts/user_update_form.html:203
+msgid "Update"
+msgstr "Atualizar"
+
+#: badger/forms.py:19 badger/models.py:40 colab/custom_settings.py:53
+msgid "Image"
+msgstr "Imagem"
+
+#: badger/forms.py:30
+msgid "You must add an Image"
+msgstr "Você deve adicionar uma imagem"
+
+#: badger/models.py:12
+msgid "Greater than or equal"
+msgstr "Maior que ou igual"
+
+#: badger/models.py:13
+msgid "less than or equal"
+msgstr "menor que ou igual"
+
+#: badger/models.py:14
+msgid "Equal"
+msgstr "Igual"
+
+#: badger/models.py:15
+msgid "Biggest"
+msgstr "Maior"
+
+#: badger/models.py:18
+msgid "Automatically"
+msgstr "Automaticamente"
+
+#: badger/models.py:19
+msgid "Manual"
+msgstr "Manual"
+
+#: badger/models.py:24
+msgid "Wikis"
+msgstr "Wikis"
+
+#: badger/models.py:25
+msgid "Revisions"
+msgstr "Conjunto de mudanças"
+
+#: badger/models.py:26 search/views.py:42
+#: search/templates/search/includes/search_filters.html:124
+msgid "Ticket"
+msgstr "Tíquetes"
+
+#: badger/models.py:36
+msgid "Title"
+msgstr "Título"
+
+#: badger/models.py:38
+msgid "Description"
+msgstr "Descrição"
+
+#: badger/models.py:41 search/forms.py:18
+msgid "Type"
+msgstr "Tipo"
+
+#: badger/models.py:43
+msgid "User attribute"
+msgstr "Atributo do usuário"
+
+#: badger/models.py:49
+msgid "Comparison"
+msgstr "Comparação"
+
+#: badger/models.py:56
+msgid "Value"
+msgstr "Valor"
+
+#: badger/models.py:62
+msgid "Awardees"
+msgstr "Premiados"
+
+#: badger/models.py:66
+msgid "Order"
+msgstr "Ordem"
+
+#: badger/models.py:69
+msgid "Badge"
+msgstr "Medalha"
+
+#: colab/custom_settings.py:9
+msgid "English"
+msgstr "Inglês"
+
+#: colab/custom_settings.py:10
+msgid "Portuguese"
+msgstr "Português"
+
+#: colab/custom_settings.py:11
+msgid "Spanish"
+msgstr "Espanhol"
+
+#: colab/custom_settings.py:34
+msgid "Recent activity"
+msgstr "Atividade recente"
+
+#: colab/custom_settings.py:38
+msgid "Relevance"
+msgstr "Relevância"
+
+#: colab/custom_settings.py:46
+msgid "Document"
+msgstr "Documento"
+
+#: colab/custom_settings.py:48
+msgid "Presentation"
+msgstr "Apresentação"
+
+#: colab/custom_settings.py:49
+msgid "Text"
+msgstr "Texto"
+
+#: colab/custom_settings.py:50 search/utils.py:9
+msgid "Code"
+msgstr "Código"
+
+#: colab/custom_settings.py:52
+msgid "Compressed"
+msgstr "Compactado"
+
+#: colab/custom_settings.py:55
+msgid "Spreadsheet"
+msgstr "Planilha"
+
+#: colab/custom_settings.py:267
+msgid "Planet Colab"
+msgstr ""
+
+#: colab/custom_settings.py:268
+msgid "Colab blog aggregator"
+msgstr "Agregador de blog Colab"
+
+#: colab/custom_settings.py:309
+msgid "One Time Snippet"
+msgstr ""
+
+#: colab/custom_settings.py:310
+msgid "In one hour"
+msgstr ""
+
+#: colab/custom_settings.py:311
+msgid "In one week"
+msgstr ""
+
+#: colab/custom_settings.py:312
+msgid "In one month"
+msgstr ""
+
+#: colab/custom_settings.py:313
+#, fuzzy
+msgid "Never"
+msgstr "Seriedade"
+
+#: planet/templates/feedzilla/_post_template.html:8
+msgid "From"
+msgstr "De"
+
+#: planet/templates/feedzilla/_post_template.html:8
+msgid "on"
+msgstr "em"
+
+#: planet/templates/feedzilla/_post_template.html:12
+msgid "Read original"
+msgstr "Leia o original"
+
+#: planet/templates/feedzilla/base.html:7
+msgid "Community Blogs"
+msgstr "Blogs da Comunidade"
+
+#: planet/templates/feedzilla/base.html:17
+msgid "Tags"
+msgstr "Etiquetas"
+
+#: planet/templates/feedzilla/base.html:21
+msgid "Source Blogs"
+msgstr "Blogs de origem"
+
+#: planet/templates/feedzilla/base.html:25
+#: planet/templates/feedzilla/submit_blog.html:5
+msgid "Submit a blog"
+msgstr "Sugerir um blog"
+
+#: planet/templates/feedzilla/index.html:10
+msgid "There is no RSS registered"
+msgstr "Não há RSS registrado"
+
+#: planet/templates/feedzilla/index.html:12
+msgid "Please"
+msgstr "Por favor"
+
+#: planet/templates/feedzilla/index.html:13
+msgid "click here"
+msgstr "clique aqui"
+
+#: planet/templates/feedzilla/index.html:14
+msgid "to submit a blog"
+msgstr "enviar um blog"
+
+#: planet/templates/feedzilla/submit_blog.html:8
+msgid ""
+"Thank you. Your application has been accepted and will be reviewed by admin "
+"in the near time."
+msgstr ""
+"Obrigado. Sua aplicação foi aceita e logo será revisada por um administrador."
+
+#: planet/templates/feedzilla/submit_blog.html:29
+msgid "Submit"
+msgstr "Enviar"
+
+#: planet/templates/feedzilla/tag.html:7
+#, python-format
+msgid "Posts with «%(tag)s» label"
+msgstr "Postagens com a etiqueta «%(tag)s»"
+
+#: planet/templates/feedzilla/tag.html:16
+msgid "No posts with such label"
+msgstr "Não há posts com essa etiqueta"
+
+#: rss/feeds.py:13
+msgid "Latest Discussions"
+msgstr "Últimas discussões"
+
+#: rss/feeds.py:32
+msgid "Discussions Most Relevance"
+msgstr "Discussões Mais Relevantes"
+
+#: rss/feeds.py:51
+msgid "Latest collaborations"
+msgstr "Últimas colaborações"
+
+#: search/forms.py:16 search/templates/search/search.html:41
+#: templates/base.html:91
+msgid "Search"
+msgstr "Busca"
+
+#: search/forms.py:19 search/views.py:22 search/views.py:33 search/views.py:69
+#: search/views.py:86 search/views.py:119
+msgid "Author"
+msgstr "Autor"
+
+#: search/forms.py:20
+msgid "Modified by"
+msgstr "Modificado por"
+
+#: search/forms.py:22 search/views.py:70
+msgid "Status"
+msgstr ""
+
+#: search/forms.py:26 search/views.py:36
+msgid "Mailinglist"
+msgstr "Grupo"
+
+#: search/forms.py:30 search/views.py:46
+msgid "Milestone"
+msgstr "Etapa"
+
+#: search/forms.py:31 search/views.py:51
+msgid "Priority"
+msgstr "Prioridade"
+
+#: search/forms.py:32 search/views.py:56
+msgid "Component"
+msgstr "Componente"
+
+#: search/forms.py:33 search/views.py:61
+msgid "Severity"
+msgstr "Seriedade"
+
+#: search/forms.py:34 search/views.py:66
+msgid "Reporter"
+msgstr "Relator"
+
+#: search/forms.py:35 search/views.py:73
+msgid "Keywords"
+msgstr "Palavras chaves"
+
+#: search/forms.py:36 search/views.py:25 search/views.py:78
+msgid "Collaborators"
+msgstr "Colaboradores"
+
+#: search/forms.py:37 search/views.py:89
+msgid "Repository"
+msgstr "Repositório"
+
+#: search/forms.py:38 search/views.py:99
+msgid "Username"
+msgstr "Usuário"
+
+#: search/forms.py:39 search/views.py:102
+msgid "Name"
+msgstr "Nome"
+
+#: search/forms.py:40 search/views.py:105
+msgid "Institution"
+msgstr "Instituição"
+
+#: search/forms.py:41 search/views.py:108
+msgid "Role"
+msgstr "Cargo"
+
+#: search/forms.py:42 search/templates/search/includes/search_filters.html:151
+#: search/templates/search/includes/search_filters.html:153
+#: search/templates/search/includes/search_filters.html:185
+#: search/templates/search/includes/search_filters.html:186
+msgid "Since"
+msgstr "Desde"
+
+#: search/forms.py:43 search/templates/search/includes/search_filters.html:160
+#: search/templates/search/includes/search_filters.html:162
+#: search/templates/search/includes/search_filters.html:189
+#: search/templates/search/includes/search_filters.html:190
+msgid "Until"
+msgstr "Até"
+
+#: search/forms.py:44 search/views.py:116
+msgid "Filename"
+msgstr "Nome do arquivo"
+
+#: search/forms.py:45 search/views.py:122
+msgid "Used by"
+msgstr "Usado por"
+
+#: search/forms.py:46 search/views.py:125
+msgid "File type"
+msgstr "Tipo do arquivo"
+
+#: search/forms.py:47 search/views.py:128
+msgid "Size"
+msgstr "Tamanho"
+
+#: search/utils.py:7 search/views.py:20
+#: search/templates/search/includes/search_filters.html:116
+#: templates/open-data.html:130
+msgid "Wiki"
+msgstr "Wiki"
+
+#: search/utils.py:10
+msgid "Tickets"
+msgstr "Tíquetes"
+
+#: search/utils.py:11
+msgid "Attachments"
+msgstr "Anexos"
+
+#: search/views.py:31 search/templates/search/includes/search_filters.html:120
+msgid "Discussion"
+msgstr "Discussões"
+
+#: search/views.py:84 search/templates/search/includes/search_filters.html:128
+msgid "Changeset"
+msgstr "Conjunto de Mudanças"
+
+#: search/views.py:95 search/templates/search/includes/search_filters.html:132
+msgid "User"
+msgstr "Usuário"
+
+#: search/views.py:112
+#: search/templates/search/includes/search_filters.html:136
+msgid "Attachment"
+msgstr "Anexo"
+
+#: search/templates/search/search.html:4
+msgid "search"
+msgstr "busca"
+
+#: search/templates/search/search.html:46
+msgid "documents found"
+msgstr "documentos encontrados"
+
+#: search/templates/search/search.html:57
+msgid "Search here"
+msgstr "Pesquise aqui"
+
+#: search/templates/search/search.html:69
+#: search/templates/search/search.html:79
+msgid "Filters"
+msgstr "Filtros"
+
+#: search/templates/search/search.html:100
+msgid "No results for your search."
+msgstr "Não há resultados para sua busca."
+
+#: search/templates/search/search.html:102
+msgid "You are searching for"
+msgstr "Você está procurando por"
+
+#: search/templates/search/includes/search_filters.html:5
+#: search/templates/search/includes/search_filters.html:33
+#: search/templates/search/includes/search_filters.html:51
+#: search/templates/search/includes/search_filters.html:69
+msgid "Remove filter"
+msgstr "Remover filtro"
+
+#: search/templates/search/includes/search_filters.html:88
+#: search/templates/search/includes/search_filters.html:171
+#: search/templates/search/includes/search_filters.html:195
+msgid "Filter"
+msgstr "Filtro"
+
+#: search/templates/search/includes/search_filters.html:94
+msgid "Sort by"
+msgstr "Ordenar por"
+
+#: search/templates/search/includes/search_filters.html:111
+msgid "Types"
+msgstr "Tipos"
+
+#: super_archives/models.py:62
+#: super_archives/templates/message-preview.html:62
+#: super_archives/templates/message-thread.html:4
+msgid "Anonymous"
+msgstr "Anônimo"
+
+#: super_archives/models.py:112
+msgid "Mailing List"
+msgstr "Lista de e-mail"
+
+#: super_archives/models.py:113
+msgid "The Mailing List where is the thread"
+msgstr "A lista de e-mail onde estão as mensagens"
+
+#: super_archives/models.py:116
+msgid "Latest message"
+msgstr "Última mensagem"
+
+#: super_archives/models.py:117
+msgid "Latest message posted"
+msgstr "Última mensagem postada"
+
+#: super_archives/models.py:118
+msgid "Score"
+msgstr "Pontuação"
+
+#: super_archives/models.py:118
+msgid "Thread score"
+msgstr "Pontuação do conjunto de mensagens"
+
+#: super_archives/models.py:127
+msgid "Thread"
+msgstr "Conjunto de mensagens"
+
+#: super_archives/models.py:128
+msgid "Threads"
+msgstr "Conjuntos de mensagens"
+
+#: super_archives/models.py:242
+msgid "Subject"
+msgstr "Assunto"
+
+#: super_archives/models.py:243
+msgid "Please enter a message subject"
+msgstr "Por favor, digite o assunto da mensagem"
+
+#: super_archives/models.py:246
+msgid "Message body"
+msgstr "Corpo da mensagem"
+
+#: super_archives/models.py:247
+msgid "Please enter a message body"
+msgstr "Por favor, digite o corpo da mensagem"
+
+#: super_archives/models.py:257
+msgid "Message"
+msgstr "Mensagem"
+
+#: super_archives/views.py:92
+msgid "Error trying to connect to Mailman API"
+msgstr "Erro na conexão com a API do Mailman"
+
+#: super_archives/views.py:95
+msgid "Timeout trying to connect to Mailman API"
+msgstr "Tempo de espera esgotado na conexão com a API do Mailman"
+
+#: super_archives/views.py:99
+msgid ""
+"Your message was sent to this topic. It may take some minutes before it's "
+"delivered by email to the group. Why don't you breath some fresh air in the "
+"meanwhile?"
+msgstr ""
+"Sua mensagem foi enviada para esse tópico. Pode levar alguns minutos até ser "
+"entregue via e-mail para o grupo. Por quê você não respira um ar fresco "
+"enquanto isso?"
+
+#: super_archives/views.py:108
+msgid "You cannot send an empty email"
+msgstr "Você não pode enviar um e-mail vazio"
+
+#: super_archives/views.py:110
+msgid "Mailing list does not exist"
+msgstr "Lista de e-mail não existe"
+
+#: super_archives/views.py:112
+msgid "Unknown error trying to connect to Mailman API"
+msgstr "Erro desconhecido na conexão com a API do Mailman"
+
+#: super_archives/views.py:151
+msgid ""
+"The email address you are trying to verify either has already been verified "
+"or does not exist."
+msgstr ""
+"O endereço de e-mail que você está tentando verificar ou já foi verificado "
+"ou não existe."
+
+#: super_archives/views.py:162
+msgid ""
+"The email address you are trying to verify is already an active email "
+"address."
+msgstr ""
+"O endereço de e-mail que você está tentando verificar já é um endereço de e-"
+"mail ativo"
+
+#: super_archives/views.py:172
+msgid "Email address verified!"
+msgstr "Endereço de e-mail verificado!"
+
+#: super_archives/management/commands/import_emails.py:207
+msgid "[Colab] Warning - Email sent with a blank subject."
+msgstr "[Colab] Aviso - E-mail enviado com o campo assunto em branco."
+
+#: super_archives/templates/message-preview.html:42
+#: super_archives/templates/message-preview.html:62
+msgid "by"
+msgstr "por"
+
+#: super_archives/templates/message-preview.html:65
+#: super_archives/templates/message-thread.html:161
+msgid "ago"
+msgstr "atrás"
+
+#: super_archives/templates/message-thread.html:35
+msgid "You must login before voting."
+msgstr "Você deve estar logado antes de votar."
+
+#: super_archives/templates/message-thread.html:132
+msgid "Order by"
+msgstr "Ordernar por"
+
+#: super_archives/templates/message-thread.html:136
+msgid "Votes"
+msgstr "Votos"
+
+#: super_archives/templates/message-thread.html:140
+msgid "Date"
+msgstr "Data"
+
+#: super_archives/templates/message-thread.html:145
+msgid "Related:"
+msgstr "Relacionado:"
+
+#: super_archives/templates/message-thread.html:156
+msgid "Statistics:"
+msgstr "Estatísticas:"
+
+#: super_archives/templates/message-thread.html:160
+msgid "started at"
+msgstr "começou à"
+
+#: super_archives/templates/message-thread.html:166
+msgid "viewed"
+msgstr "visualizado"
+
+#: super_archives/templates/message-thread.html:167
+#: super_archives/templates/message-thread.html:172
+#: super_archives/templates/message-thread.html:177
+msgid "times"
+msgstr "vezes"
+
+#: super_archives/templates/message-thread.html:171
+msgid "answered"
+msgstr "respondido"
+
+#: super_archives/templates/message-thread.html:176
+msgid "voted"
+msgstr "votado"
+
+#: super_archives/templates/message-thread.html:182
+msgid "Tags:"
+msgstr "Etiquetas:"
+
+#: super_archives/templates/superarchives/thread-dashboard.html:4
+#: super_archives/templates/superarchives/thread-dashboard.html:7
+#: templates/base.html:66
+msgid "Groups"
+msgstr "Grupos"
+
+#: super_archives/templates/superarchives/thread-dashboard.html:17
+msgid "latest"
+msgstr "mais recentes"
+
+#: super_archives/templates/superarchives/thread-dashboard.html:25
+#: super_archives/templates/superarchives/thread-dashboard.html:39
+msgid "more..."
+msgstr "mais..."
+
+#: super_archives/templates/superarchives/thread-dashboard.html:31
+msgid "most relevant"
+msgstr "mais relevantes"
+
+#: super_archives/templates/superarchives/emails/email_blank_subject.txt:2
+msgid "Hello"
+msgstr "Olá"
+
+#: super_archives/templates/superarchives/emails/email_blank_subject.txt:3
+#, python-format
+msgid ""
+"\n"
+"You've sent an email to %(mailinglist)s with a blank subject and the "
+"following content:\n"
+"\n"
+"\"%(body)s\"\n"
+"\n"
+"Please, fill the subject in every email you send it.\n"
+"\n"
+"Thank you.\n"
+msgstr ""
+"\n"
+"Você enviou um e-mail para %(mailinglist)s com o campo Assunto em branco e o "
+"seguinte conteúdo:\n"
+"\n"
+"\"%(body)s\"\n"
+"\n"
+"Por favor, preencha o assunto em todos os e-mails que você enviar.\n"
+"\n"
+"Obrigado.\n"
+
+#: super_archives/templates/superarchives/emails/email_verification.txt:2
+#, python-format
+msgid ""
+"Hey, we want to verify that you are indeed \"%(fullname)s (%(username)s)\". "
+"If that's the case, please follow the link below:"
+msgstr ""
+"Hey, queremos verificar se você realmente é \"%(fullname)s (%(username)s)\". "
+"Se esse é o caso, por favor, clique no link abaixo:"
+
+#: super_archives/templates/superarchives/emails/email_verification.txt:6
+#, python-format
+msgid ""
+"If you're not %(username)s or didn't request verification you can ignore "
+"this email."
+msgstr ""
+"Se você não é %(username)s ou não pediu uma verificação você pode ignorar "
+"esse e-mail"
+
+#: super_archives/templates/superarchives/includes/message.html:17
+#: super_archives/templates/superarchives/includes/message.html:18
+msgid "Link to this message"
+msgstr "Link para essa mensagem"
+
+#: super_archives/templates/superarchives/includes/message.html:46
+msgid "Reply"
+msgstr "Responder"
+
+#: super_archives/templates/superarchives/includes/message.html:63
+msgid "Send a message"
+msgstr "Enviar uma mensagem"
+
+#: super_archives/templates/superarchives/includes/message.html:66
+msgid ""
+"After sending a message it will take few minutes before it shows up in here. "
+"Why don't you grab a coffee?"
+msgstr ""
+"Depois de enviar uma mensagem levará alguns minutos antes dela aparecer "
+"aqui. Por que você não pega um café?"
+
+#: super_archives/templates/superarchives/includes/message.html:69
+msgid "Send"
+msgstr "Enviar"
+
+#: super_archives/utils/email.py:14
+msgid "Please verify your email "
+msgstr "Por favor, verifique seu e-mail "
+
+#: super_archives/utils/email.py:25
+msgid "Registration on the mailing list"
+msgstr "Inscrição na lista de e-mail"
+
+#: templates/404.html:5
+msgid "Not found. Keep searching! :)"
+msgstr "Não encontrado. Continue procurando! :)"
+
+#: templates/500.html:2
+msgid "Ooopz... something went wrong!"
+msgstr "Opa... algo saiu errado!"
+
+#: templates/base.html:63
+msgid "Timeline"
+msgstr "Linha do Tempo"
+
+#: templates/base.html:69
+msgid "Blogs"
+msgstr ""
+
+#: templates/base.html:72
+msgid "Contribute"
+msgstr "Contribua"
+
+#: templates/base.html:76
+msgid "New Wiki Page"
+msgstr "Nova Página Wiki"
+
+#: templates/base.html:78
+msgid "View Tickets"
+msgstr "Ver Tiquetes"
+
+#: templates/base.html:80
+msgid "New Ticket"
+msgstr "Novo Tíquete"
+
+#: templates/base.html:82
+msgid "Roadmap"
+msgstr "Planejamento"
+
+#: templates/base.html:86
+msgid "Browse Source"
+msgstr "Códigos Fontes"
+
+#: templates/base.html:87
+msgid "Continuous Integration"
+msgstr "Integração Contínua"
+
+#: templates/base.html:95
+msgid "Help"
+msgstr "Ajuda"
+
+#: templates/base.html:107 templates/base.html.py:112
+msgid "Login"
+msgstr "Entrar"
+
+#: templates/base.html:126
+msgid "My Profile"
+msgstr "Meu Perfil"
+
+#: templates/base.html:127
+msgid "Logout"
+msgstr "Sair"
+
+#: templates/base.html:139 templates/base.html.py:142
+msgid "Search here..."
+msgstr "Pesquise aqui..."
+
+#: templates/base.html:155
+msgid "The login has failed. Please, try again."
+msgstr "O login falhou. Por favor, tente novamente."
+
+#: templates/base.html:182
+msgid "Last email imported at"
+msgstr "Último e-mail importado em"
+
+#: templates/base.html:189
+msgid "The contents of this site is published under license"
+msgstr "O conteúdo deste site está publicado sob a licença"
+
+#: templates/base.html:192
+msgid ""
+"Creative Commons 3.0 Brasil - Atribuição - Não-Comercial - Compartilha-Igual"
+msgstr ""
+
+#: templates/home.html:21
+msgid "Latest Collaborations"
+msgstr "Últimas Colaborações"
+
+#: templates/home.html:25
+msgid "RSS - Latest collaborations"
+msgstr "RSS - Últimas Colaborações"
+
+#: templates/home.html:34
+msgid "View more collaborations..."
+msgstr "Ver mais colaborações..."
+
+#: templates/home.html:41
+msgid "Collaboration Graph"
+msgstr "Gráfico de Colaborações"
+
+#: templates/home.html:52
+msgid "Most Relevant Threads"
+msgstr "Discussões Mais Relevantes"
+
+#: templates/home.html:56
+msgid "RSS - Most Relevant Threads"
+msgstr "RSS - Discussões Mais Relevantes"
+
+#: templates/home.html:64 templates/home.html.py:83
+msgid "View more discussions..."
+msgstr "Ver mais discussões..."
+
+#: templates/home.html:71
+msgid "Latest Threads"
+msgstr "Últimas Discussões"
+
+#: templates/home.html:75
+msgid "RSS - Latest Threads"
+msgstr "RSS - Últimas Discussões"
+
+#: templates/open-data.html:6
+msgid "OpenData"
+msgstr "OpenData"
+
+#: templates/open-data.html:7
+msgid ""
+"If you are interested in any other data that is not provided by this API, "
+"please contact us via the ticketing system (you must be registered in order "
+"to create a ticket)."
+msgstr ""
+
+#: templates/open-data.html:9
+msgid "Retrieving data via API"
+msgstr ""
+
+#: templates/open-data.html:10
+msgid "Colab API works through HTTP/REST, always returning JSON objects."
+msgstr ""
+
+#: templates/open-data.html:12
+msgid "The base API URL is"
+msgstr ""
+
+#: templates/open-data.html:19
+msgid ""
+"Each model listed below has a resource_uri field available, which is the "
+"object's data URI."
+msgstr ""
+
+#: templates/open-data.html:20
+msgid ""
+"The following list contains the available models to retrieve data and its "
+"fields available for filtering"
+msgstr ""
+
+#: templates/open-data.html:24 templates/open-data.html.py:39
+#: templates/open-data.html:50 templates/open-data.html.py:62
+#: templates/open-data.html:74 templates/open-data.html.py:95
+msgid "Fields"
+msgstr ""
+
+#: templates/open-data.html:25
+msgid ""
+"The email field is not shown for user's privacy, but you can use it to filter"
+msgstr ""
+
+#: templates/open-data.html:27
+msgid "The user's username"
+msgstr ""
+
+#: templates/open-data.html:28
+#, fuzzy
+msgid "The user's email address"
+msgstr "Adicionar outro endereço de e-mail"
+
+#: templates/open-data.html:29
+msgid "What is the user's institution"
+msgstr ""
+
+#: templates/open-data.html:30
+msgid "What is the user's role"
+msgstr ""
+
+#: templates/open-data.html:31
+msgid "The user's first name"
+msgstr ""
+
+#: templates/open-data.html:32
+msgid "The user's last name"
+msgstr ""
+
+#: templates/open-data.html:33
+msgid "A mini bio of the user"
+msgstr ""
+
+#: templates/open-data.html:40
+msgid ""
+"The address field is not shown for user's privacy, but you can use it to "
+"filter"
+msgstr ""
+
+#: templates/open-data.html:42
+msgid "It has a relationshop with the user described above"
+msgstr ""
+
+#: templates/open-data.html:43
+#, fuzzy
+msgid "An email address"
+msgstr "Adicionar outro endereço de e-mail"
+
+#: templates/open-data.html:44
+msgid "The user's real name"
+msgstr ""
+
+#: templates/open-data.html:52
+msgid "It has a relationship with the emailaddress described above"
+msgstr ""
+
+#: templates/open-data.html:53
+#, fuzzy
+msgid "The message's body"
+msgstr "Corpo da mensagem"
+
+#: templates/open-data.html:54
+#, fuzzy
+msgid "The message's subject"
+msgstr "Por favor, digite o assunto da mensagem"
+
+#: templates/open-data.html:55
+#, fuzzy
+msgid "The message's id"
+msgstr "Última mensagem"
+
+#: templates/open-data.html:56
+msgid "The message's received time"
+msgstr ""
+
+#: templates/open-data.html:64
+msgid "The revision's author username"
+msgstr ""
+
+#: templates/open-data.html:65
+msgid "When the revision's were created"
+msgstr ""
+
+#: templates/open-data.html:66
+msgid "The revision's key"
+msgstr ""
+
+#: templates/open-data.html:67
+msgid "The revision's message"
+msgstr ""
+
+#: templates/open-data.html:68
+msgid "The revision's repository name"
+msgstr ""
+
+#: templates/open-data.html:76
+msgid "The ticket's author username"
+msgstr ""
+
+#: templates/open-data.html:77
+msgid "The ticket's component"
+msgstr ""
+
+#: templates/open-data.html:78
+msgid "When the ticket's were created"
+msgstr ""
+
+#: templates/open-data.html:79
+msgid "The ticket's description"
+msgstr ""
+
+#: templates/open-data.html:80
+#, fuzzy
+msgid "The ticket's id"
+msgstr "Tíquetes"
+
+#: templates/open-data.html:81
+msgid "The ticket's keywords"
+msgstr ""
+
+#: templates/open-data.html:82
+msgid "The ticket's milestone"
+msgstr ""
+
+#: templates/open-data.html:83 templates/open-data.html.py:99
+msgid "The time of the last modification"
+msgstr ""
+
+#: templates/open-data.html:84
+msgid "The username of the last user who modified the ticket"
+msgstr ""
+
+#: templates/open-data.html:85
+msgid "The ticket's priority"
+msgstr ""
+
+#: templates/open-data.html:86
+msgid "The ticket's severity"
+msgstr ""
+
+#: templates/open-data.html:87
+msgid "The ticket's status"
+msgstr ""
+
+#: templates/open-data.html:88
+msgid "The ticket's summary"
+msgstr ""
+
+#: templates/open-data.html:89
+msgid "The ticket's version"
+msgstr ""
+
+#: templates/open-data.html:97
+msgid "The wiki's author username"
+msgstr ""
+
+#: templates/open-data.html:98
+msgid "When the wiki's were created"
+msgstr ""
+
+#: templates/open-data.html:100
+msgid "The username of the last user who modified the wiki"
+msgstr ""
+
+#: templates/open-data.html:101
+msgid "The wiki's name"
+msgstr ""
+
+#: templates/open-data.html:102
+msgid "the wiki's content"
+msgstr ""
+
+#: templates/open-data.html:109
+msgid "Parameters"
+msgstr ""
+
+#: templates/open-data.html:112
+msgid "Results per page"
+msgstr ""
+
+#: templates/open-data.html:113
+msgid "Number of results to be displayed per page."
+msgstr ""
+
+#: templates/open-data.html:114
+msgid "Default: 20"
+msgstr ""
+
+#: templates/open-data.html:118
+msgid "Starts of n element"
+msgstr ""
+
+#: templates/open-data.html:119
+msgid "Where n is the index of the first result to appear in the page."
+msgstr ""
+
+#: templates/open-data.html:120
+msgid "Default: 0"
+msgstr ""
+
+#: templates/open-data.html:122
+#, fuzzy
+msgid "Filtering"
+msgstr "Filtro"
+
+#: templates/open-data.html:124
+msgid "The field name"
+msgstr ""
+
+#: templates/open-data.html:125
+msgid ""
+"If you are looking for a specific wiki, and you know the wiki's name, you "
+"can filter it as below"
+msgstr ""
+
+#: templates/open-data.html:126
+#, fuzzy
+msgid "WikiName"
+msgstr "Nome"
+
+#: templates/open-data.html:127
+msgid ""
+"Where "name" is the fieldname and "WikiName" is the "
+"value you want to filter."
+msgstr ""
+
+#: templates/open-data.html:128
+#, fuzzy
+msgid "Usage"
+msgstr "Mensagem"
+
+#: templates/open-data.html:129
+msgid ""
+"You can also filter using Django lookup fields with the double underscores, "
+"just as below"
+msgstr ""
+
+#: templates/open-data.html:131 templates/open-data.html.py:132
+#, fuzzy
+msgid "test"
+msgstr "mais recentes"
+
+#: templates/open-data.html:133
+msgid "Usage with relationships"
+msgstr ""
+
+#: templates/open-data.html:134
+msgid ""
+"You can use related fields to filter too. So, you can filter by any field of "
+"emailaddress using the 'from_address' field of message, which has a relation "
+"to emailaddress. You will achieve the related fields by using double "
+"underscore and the field's name. See the example below"
+msgstr ""
+
+#: templates/open-data.html:136
+msgid ""
+"So, real_name is a field of emailaddress, and you had access to this field "
+"by a message field called from_address and using double underscore to say "
+"you want to use a field of that relationship"
+msgstr ""
+
+#: templates/open-data.html:137
+msgid ""
+"Note: email filters must be exact. Which means that __contains, "
+"__startswith, __endswith and others won't work"
+msgstr ""
+
+#: templates/open-data.html:138
+msgid ""
+"Another example of usage with relations. Used to retrieve all messages of a "
+"given user, using the username or the email field"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:37
+#, fuzzy
+msgid "Compare"
+msgstr "Comparação"
+
+#: templates/dpaste/snippet_details.html:47
+#, python-format
+msgid "Expires in: %(date)s"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:49
+msgid "Snippet never expires"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:51
+msgid "One-time snippet"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:56
+msgid "Really delete this snippet?"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:58
+#, fuzzy
+msgid "Delete Now"
+msgstr "Apagar"
+
+#: templates/dpaste/snippet_details.html:63
+#: templates/dpaste/snippet_details.html:65
+#, fuzzy
+msgid "Compare Snippets"
+msgstr "Comparação"
+
+#: templates/dpaste/snippet_details.html:69
+#: templates/dpaste/snippet_details.html:71
+msgid "View Raw"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:75
+msgid "Gist"
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:88
+msgid "This is a one-time snippet."
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:90
+msgid "It will automatically get deleted after {{ remaining }} further views."
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:92
+msgid "It will automatically get deleted after the next view."
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:94
+msgid "It cannot be viewed again."
+msgstr ""
+
+#: templates/dpaste/snippet_details.html:109
+msgid "Reply to this snippet"
+msgstr ""
+
+#: templates/dpaste/snippet_diff.html:5
+#, python-format
+msgid ""
+"\n"
+" Diff between #%(filea_id)s and #%(fileb_id)s \n"
+" "
+msgstr ""
+
+#: templates/dpaste/snippet_form.html:28
+msgid "Paste it"
+msgstr ""
+
+#~ msgid "Creative Commons - attribution, non-commercial"
+#~ msgstr "Creative Commons - atribuição e não-comercial"
+
+#~ msgid "Willing to help"
+#~ msgstr "Vontade de ajudar"
+
+#~ msgid "Identi.ca account"
+#~ msgstr "Conta Identi.ca"
+
+#~ msgid "Biography"
+#~ msgstr "Biografia"
+
+#~ msgid "Other Collaborations"
+#~ msgstr "Outras Colaborações"
+
+#~ msgid "Mailing List Subscriptions"
+#~ msgstr "Inscrições em listas de e-mails"
+
+#~ msgid "Change SVN and XMPP Client password"
+#~ msgstr "Trocar a senha do SVN e do Mensageiro"
+
+#~ msgid ""
+#~ "You don't need to change this password. Change it only if you really know "
+#~ "what you are doing."
+#~ msgstr ""
+#~ "Você não precisa trocar essa senha. Troque-a somente se tem certeza do "
+#~ "que está fazendo."
+
+#~ msgid "Subscribes: "
+#~ msgstr "Inscrições: "
+
+#~ msgid "Discussions"
+#~ msgstr "Discussões"
+
+#~ msgid "Community inside participations"
+#~ msgstr "Participações internas da comunidade"
+
+#~ msgid "documents found in"
+#~ msgstr "documentos encontrados em"
+
+#~ msgid "seconds"
+#~ msgstr "segundos"
+
+#~ msgid "Previous"
+#~ msgstr "Anterior"
+
+#~ msgid "Page"
+#~ msgstr "Página"
+
+#~ msgid "of"
+#~ msgstr "de"
+
+#~ msgid "Next"
+#~ msgstr "Pŕoximo"
+
+#~ msgid "%(option)s"
+#~ msgstr "%(option)s"
+
+#~ msgid "%(name)s"
+#~ msgstr "%(name)s"
+
+#~ msgid "%(name)s "
+#~ msgstr "%(name)s "
diff --git a/colab/manage.py b/colab/manage.py
new file mode 100644
index 0000000..a466007
--- /dev/null
+++ b/colab/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "colab.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/colab/planet/__init__.py b/colab/planet/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/planet/__init__.py
diff --git a/colab/planet/admin.py b/colab/planet/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/colab/planet/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/colab/planet/locale/es/LC_MESSAGES/django.po b/colab/planet/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000..9f516d1
--- /dev/null
+++ b/colab/planet/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Leonardo J. Caballero G. , 2013.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-07-26 11:28-0300\n"
+"PO-Revision-Date: 2013-10-15 23:46-0430\n"
+"Last-Translator: Leonardo J. Caballero G. \n"
+"Language-Team: ES \n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Virtaal 0.7.1\n"
+
+#: templates/common/pagination.html:5 templates/common/pagination.html.py:7
+msgid "previous"
+msgstr "anterior"
+
+#: templates/common/pagination.html:31 templates/common/pagination.html:33
+msgid "next"
+msgstr "próximo"
+
+#: templates/feedzilla/_post_template.html:9
+msgid "From"
+msgstr "De"
+
+#: templates/feedzilla/_post_template.html:9
+msgid "on"
+msgstr "en"
+
+#: templates/feedzilla/_post_template.html:17
+msgid "Read original"
+msgstr "Leer original"
+
+#: templates/feedzilla/base.html:6
+msgid "Planet"
+msgstr "Planeta"
+
+#: templates/feedzilla/base.html:15 templates/feedzilla/submit_blog.html:5
+msgid "Submit a blog"
+msgstr "Solicite la inclusión de un blog"
+
+#: templates/feedzilla/base.html:18
+msgid "Tags"
+msgstr "Etiquetas"
+
+#: templates/feedzilla/base.html:22
+msgid "Source Blogs"
+msgstr "Fuente de Blogs"
+
+#: templates/feedzilla/submit_blog.html:8
+msgid ""
+"Thank you. Your application has been accepted and will be reviewed by admin "
+"in the near time."
+msgstr "Gracias. Su solicitud ha sido aceptado y sera revisado por el administrador, lo mas pronto posible."
+
+#: templates/feedzilla/submit_blog.html:10
+msgid "Required fields"
+msgstr "Campos requeridos"
+
+#: templates/feedzilla/submit_blog.html:14
+msgid "Blog Information"
+msgstr "Información del blog"
+
+#: templates/feedzilla/submit_blog.html:15
+msgid "Blog URL"
+msgstr "Dirección URL de Blog"
+
+#: templates/feedzilla/submit_blog.html:16
+msgid "Blog name"
+msgstr "Nombre de blog"
+
+#: templates/feedzilla/submit_blog.html:17
+msgid "Name of author of the blog"
+msgstr "Nombre del autor del blog"
+
+#: templates/feedzilla/submit_blog.html:18
+msgid "Feed URL"
+msgstr "Dirección URL de Feed"
+
+#: templates/feedzilla/submit_blog.html:18
+msgid "You can specify what exactly feed you want submit"
+msgstr ""
+"Usted puede especificar cual feed exactamente usted quiere solicitar su "
+"inclusión"
+
+#: templates/feedzilla/submit_blog.html:19
+msgid "Submit"
+msgstr "Solicite la inclusión de un blog"
+
+#: templates/feedzilla/tag.html:5
+#, python-format
+msgid "Posts with «%(tag)s» label"
+msgstr "Envíos con etiqueta «%(tag)s»"
+
+#: templates/feedzilla/tag.html:14
+msgid "No posts with such label"
+msgstr "No hay envíos con dicha etiqueta"
diff --git a/colab/planet/locale/pt_BR/LC_MESSAGES/django.mo b/colab/planet/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..ed75b5d
Binary files /dev/null and b/colab/planet/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/colab/planet/locale/pt_BR/LC_MESSAGES/django.po b/colab/planet/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..bf99c46
--- /dev/null
+++ b/colab/planet/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,119 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-08-20 14:52-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: templates/common/pagination.html:5 templates/common/pagination.html.py:7
+msgid "previous"
+msgstr "anterior"
+
+#: templates/common/pagination.html:31 templates/common/pagination.html:33
+msgid "next"
+msgstr "próxima"
+
+#: templates/feedzilla/_post_template.html:9
+msgid "From"
+msgstr "De"
+
+#: templates/feedzilla/_post_template.html:9
+msgid "on"
+msgstr "em"
+
+#: templates/feedzilla/_post_template.html:13
+msgid "Read original"
+msgstr "Ler original"
+
+#: templates/feedzilla/base.html:6
+msgid "Planet"
+msgstr "Planet"
+
+#: templates/feedzilla/base.html:14 templates/feedzilla/submit_blog.html:7
+msgid "Submit a blog"
+msgstr "Solicite a inclusão de um blog"
+
+#: templates/feedzilla/base.html:17
+msgid "Tags"
+msgstr "Tags"
+
+#: templates/feedzilla/base.html:21
+msgid "Source Blogs"
+msgstr "Blogs Fonte"
+
+#: templates/feedzilla/index.html:11
+msgid "There's no RSS registered"
+msgstr "Não há RSS cadastrado"
+
+#: templates/feedzilla/index.html:12
+msgid "Please"
+msgstr "Por favor"
+
+#: templates/feedzilla/index.html:13
+msgid "click here"
+msgstr "clique aqui"
+
+#: templates/feedzilla/index.html:14
+msgid "to submit a blog"
+msgstr "para a inclusão de um blog"
+
+#: templates/feedzilla/submit_blog.html:11
+msgid ""
+"Thank you. Your application has been accepted and will be reviewed by admin "
+"in the near time."
+msgstr ""
+
+#: templates/feedzilla/submit_blog.html:13
+msgid "Required fields"
+msgstr ""
+
+#: templates/feedzilla/submit_blog.html:17
+msgid "Blog Information"
+msgstr "Informações do blog"
+
+#: templates/feedzilla/submit_blog.html:18
+msgid "Blog URL"
+msgstr ""
+
+#: templates/feedzilla/submit_blog.html:19
+#, fuzzy
+msgid "Blog name"
+msgstr "Blogs"
+
+#: templates/feedzilla/submit_blog.html:20
+msgid "Name of author of the blog"
+msgstr ""
+
+#: templates/feedzilla/submit_blog.html:21
+msgid "Feed URL"
+msgstr ""
+
+#: templates/feedzilla/submit_blog.html:21
+msgid "You can specify what exactly feed you want submit"
+msgstr ""
+
+#: templates/feedzilla/submit_blog.html:22
+#, fuzzy
+msgid "Submit"
+msgstr "Solicite a inclusão de um blog"
+
+#: templates/feedzilla/tag.html:7
+#, python-format
+msgid "Posts with «%(tag)s» label"
+msgstr ""
+
+#: templates/feedzilla/tag.html:16
+msgid "No posts with such label"
+msgstr ""
diff --git a/colab/planet/models.py b/colab/planet/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/colab/planet/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/colab/planet/templates/common/pagination.html b/colab/planet/templates/common/pagination.html
new file mode 100644
index 0000000..270f64e
--- /dev/null
+++ b/colab/planet/templates/common/pagination.html
@@ -0,0 +1,34 @@
+{% load i18n %}
+{% if page.paginator.num_pages > 1 %}
+
+
+
+{% endif %}
diff --git a/colab/planet/templates/feedzilla/_post_template.html b/colab/planet/templates/feedzilla/_post_template.html
new file mode 100644
index 0000000..33444cc
--- /dev/null
+++ b/colab/planet/templates/feedzilla/_post_template.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+
+
+
+
+
+
+
+
+ {{ post.summary|safe }}
+
+
{% trans "Read original" %}
+
+ {% if post.tags.count %}
+
+ {% endif %}
+
diff --git a/colab/planet/templates/feedzilla/base.html b/colab/planet/templates/feedzilla/base.html
new file mode 100644
index 0000000..79c2574
--- /dev/null
+++ b/colab/planet/templates/feedzilla/base.html
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% load i18n feedzilla_tags %}
+
+{% block title %}Blogs{% endblock %}
+
+{% block main-content %}
+ {% trans 'Community Blogs' %}
+
+
+
+
+ {% block feedzilla_content %}{% endblock %}
+
+
+
+
+
{% trans 'Tags' %}
+ {% feedzilla_tag_cloud %}
+
+
+
{% trans 'Source Blogs' %}
+ {% feedzilla_donor_list order_by="title" %}
+ {% if user.is_authenticated %}
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/colab/planet/templates/feedzilla/index.html b/colab/planet/templates/feedzilla/index.html
new file mode 100644
index 0000000..e387da1
--- /dev/null
+++ b/colab/planet/templates/feedzilla/index.html
@@ -0,0 +1,20 @@
+{% extends 'feedzilla/base.html' %}
+{% load i18n %}
+
+{% block feedzilla_content %}
+
+{% for post in page.object_list %}
+ {% include 'feedzilla/_post_template.html' %}
+
+ {% empty %}
+ {% trans 'There is no RSS registered' %}
+
+ {% trans 'Please' %}
+ {% trans 'click here' %}
+ {% trans 'to submit a blog' %}
+
+{% endfor %}
+
+{% include "common/pagination.html" %}
+
+{% endblock %}
diff --git a/colab/planet/templates/feedzilla/submit_blog.html b/colab/planet/templates/feedzilla/submit_blog.html
new file mode 100644
index 0000000..a638dbb
--- /dev/null
+++ b/colab/planet/templates/feedzilla/submit_blog.html
@@ -0,0 +1,37 @@
+{% extends 'feedzilla/base.html' %}
+{% load i18n %}
+
+{% block feedzilla_content %}
+{% trans "Submit a blog" %}
+
+{% if success %}
+{% trans "Thank you. Your application has been accepted and will be reviewed by admin in the near time." %}
+{% else %}
+
+{% endif %}
+{% endblock %}
diff --git a/colab/planet/templates/feedzilla/tag.html b/colab/planet/templates/feedzilla/tag.html
new file mode 100644
index 0000000..3cc3666
--- /dev/null
+++ b/colab/planet/templates/feedzilla/tag.html
@@ -0,0 +1,19 @@
+{% extends 'feedzilla/base.html' %}
+{% load i18n %}
+
+{% block feedzilla_content %}
+
+
+
{% block title %}{% blocktrans %}Posts with «{{ tag }}» label{% endblocktrans %}{% endblock title %}
+
+ {% if page.object_list %}
+ {% for post in page.object_list %}
+ {% include 'feedzilla/_post_template.html' %}
+
+ {% endfor %}
+ {% include "pagination.html" %}
+ {% else %}
+
{% trans "No posts with such label" %}
+ {% endif %}
+
+{% endblock %}
diff --git a/colab/planet/tests.py b/colab/planet/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/planet/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/planet/views.py b/colab/planet/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/colab/planet/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/colab/proxy/__init__.py b/colab/proxy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/proxy/__init__.py
diff --git a/colab/proxy/context_processors.py b/colab/proxy/context_processors.py
new file mode 100644
index 0000000..317f0f9
--- /dev/null
+++ b/colab/proxy/context_processors.py
@@ -0,0 +1,12 @@
+
+from django.apps import apps
+
+
+def proxied_apps(request):
+ proxied_apps = {}
+
+ for app in apps.get_app_configs():
+ if getattr(app, 'colab_proxied_app', False):
+ proxied_apps[app.label] = True
+
+ return {'proxy': proxied_apps}
diff --git a/colab/proxy/gitlab/__init__.py b/colab/proxy/gitlab/__init__.py
new file mode 100644
index 0000000..a2b3db3
--- /dev/null
+++ b/colab/proxy/gitlab/__init__.py
@@ -0,0 +1,3 @@
+
+
+default_app_config = 'proxy.gitlab.apps.ProxyGitlabAppConfig'
diff --git a/colab/proxy/gitlab/admin.py b/colab/proxy/gitlab/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/colab/proxy/gitlab/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/colab/proxy/gitlab/apps.py b/colab/proxy/gitlab/apps.py
new file mode 100644
index 0000000..aea37de
--- /dev/null
+++ b/colab/proxy/gitlab/apps.py
@@ -0,0 +1,7 @@
+
+from ..utils.apps import ColabProxiedAppConfig
+
+
+class ProxyGitlabAppConfig(ColabProxiedAppConfig):
+ name = 'proxy.gitlab'
+ verbose_name = 'Gitlab Proxy'
diff --git a/colab/proxy/gitlab/diazo.xml b/colab/proxy/gitlab/diazo.xml
new file mode 100644
index 0000000..a4e8c82
--- /dev/null
+++ b/colab/proxy/gitlab/diazo.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/colab/proxy/gitlab/models.py b/colab/proxy/gitlab/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/colab/proxy/gitlab/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/colab/proxy/gitlab/templates/proxy/gitlab.html b/colab/proxy/gitlab/templates/proxy/gitlab.html
new file mode 100644
index 0000000..6694ca5
--- /dev/null
+++ b/colab/proxy/gitlab/templates/proxy/gitlab.html
@@ -0,0 +1,47 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block head_css %}
+
+{% endblock %}
+
+{% block head_js %}
+
+
+
+
+{% endblock %}
diff --git a/colab/proxy/gitlab/tests.py b/colab/proxy/gitlab/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/proxy/gitlab/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/proxy/gitlab/urls.py b/colab/proxy/gitlab/urls.py
new file mode 100644
index 0000000..afe6440
--- /dev/null
+++ b/colab/proxy/gitlab/urls.py
@@ -0,0 +1,10 @@
+
+from django.conf.urls import patterns, url
+
+from .views import GitlabProxyView
+
+
+urlpatterns = patterns('',
+ # Gitlab URLs
+ url(r'^gitlab/(?P.*)$', GitlabProxyView.as_view()),
+)
diff --git a/colab/proxy/gitlab/views.py b/colab/proxy/gitlab/views.py
new file mode 100644
index 0000000..c09c19c
--- /dev/null
+++ b/colab/proxy/gitlab/views.py
@@ -0,0 +1,9 @@
+
+from django.conf import settings
+
+from ..utils.views import ColabProxyView
+
+
+class GitlabProxyView(ColabProxyView):
+ upstream = settings.PROXIED_APPS['gitlab']['upstream']
+ diazo_theme_template = 'proxy/gitlab.html'
diff --git a/colab/proxy/jenkins/__init__.py b/colab/proxy/jenkins/__init__.py
new file mode 100644
index 0000000..093a0b1
--- /dev/null
+++ b/colab/proxy/jenkins/__init__.py
@@ -0,0 +1,3 @@
+
+
+default_app_config = 'proxy.jenkins.apps.ProxyJenkinsAppConfig'
diff --git a/colab/proxy/jenkins/admin.py b/colab/proxy/jenkins/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/colab/proxy/jenkins/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/colab/proxy/jenkins/apps.py b/colab/proxy/jenkins/apps.py
new file mode 100644
index 0000000..b9fb226
--- /dev/null
+++ b/colab/proxy/jenkins/apps.py
@@ -0,0 +1,7 @@
+
+from ..utils.apps import ColabProxiedAppConfig
+
+
+class ProxyJenkinsAppConfig(ColabProxiedAppConfig):
+ name = 'proxy.jenkins'
+ verbose_name = 'Jenkins Proxy'
diff --git a/colab/proxy/jenkins/diazo.xml b/colab/proxy/jenkins/diazo.xml
new file mode 100644
index 0000000..7eacea4
--- /dev/null
+++ b/colab/proxy/jenkins/diazo.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/colab/proxy/jenkins/models.py b/colab/proxy/jenkins/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/colab/proxy/jenkins/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/colab/proxy/jenkins/tests.py b/colab/proxy/jenkins/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/proxy/jenkins/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/proxy/jenkins/urls.py b/colab/proxy/jenkins/urls.py
new file mode 100644
index 0000000..8c499e4
--- /dev/null
+++ b/colab/proxy/jenkins/urls.py
@@ -0,0 +1,10 @@
+
+from django.conf.urls import patterns, url
+
+from .views import JenkinsProxyView
+
+
+urlpatterns = patterns('',
+ # Jenkins URLs
+ url(r'^ci/(?P.*)$', JenkinsProxyView.as_view()),
+)
diff --git a/colab/proxy/jenkins/views.py b/colab/proxy/jenkins/views.py
new file mode 100644
index 0000000..079fc8b
--- /dev/null
+++ b/colab/proxy/jenkins/views.py
@@ -0,0 +1,8 @@
+
+from django.conf import settings
+
+from ..utils.views import ColabProxyView
+
+
+class JenkinsProxyView(ColabProxyView):
+ upstream = settings.PROXIED_APPS['jenkins']['upstream']
diff --git a/colab/proxy/redmine/__init__.py b/colab/proxy/redmine/__init__.py
new file mode 100644
index 0000000..38dd692
--- /dev/null
+++ b/colab/proxy/redmine/__init__.py
@@ -0,0 +1,3 @@
+
+
+default_app_config = 'proxy.redmine.apps.ProxyRedmineAppConfig'
diff --git a/colab/proxy/redmine/admin.py b/colab/proxy/redmine/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/colab/proxy/redmine/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/colab/proxy/redmine/apps.py b/colab/proxy/redmine/apps.py
new file mode 100644
index 0000000..62382e9
--- /dev/null
+++ b/colab/proxy/redmine/apps.py
@@ -0,0 +1,7 @@
+
+from ..utils.apps import ColabProxiedAppConfig
+
+
+class ProxyRedmineAppConfig(ColabProxiedAppConfig):
+ name = 'proxy.redmine'
+ verbose_name = 'Redmine Proxy'
diff --git a/colab/proxy/redmine/diazo.xml b/colab/proxy/redmine/diazo.xml
new file mode 100644
index 0000000..ba2fad0
--- /dev/null
+++ b/colab/proxy/redmine/diazo.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/colab/proxy/redmine/models.py b/colab/proxy/redmine/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/colab/proxy/redmine/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/colab/proxy/redmine/tests.py b/colab/proxy/redmine/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/proxy/redmine/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/proxy/redmine/urls.py b/colab/proxy/redmine/urls.py
new file mode 100644
index 0000000..d8e908b
--- /dev/null
+++ b/colab/proxy/redmine/urls.py
@@ -0,0 +1,10 @@
+
+from django.conf.urls import patterns, url
+
+from .views import RedmineProxyView
+
+
+urlpatterns = patterns('',
+ # RedmineProxyView URLs
+ url(r'^redmine/(?P.*)$', RedmineProxyView.as_view()),
+)
diff --git a/colab/proxy/redmine/views.py b/colab/proxy/redmine/views.py
new file mode 100644
index 0000000..5899440
--- /dev/null
+++ b/colab/proxy/redmine/views.py
@@ -0,0 +1,9 @@
+
+from django.conf import settings
+
+from ..utils.views import ColabProxyView
+
+
+class RedmineProxyView(ColabProxyView):
+ upstream = settings.PROXIED_APPS['redmine']['upstream']
+ diazo_theme_template = 'proxy/redmine.html'
diff --git a/colab/proxy/trac/__init__.py b/colab/proxy/trac/__init__.py
new file mode 100644
index 0000000..5cb7f41
--- /dev/null
+++ b/colab/proxy/trac/__init__.py
@@ -0,0 +1,3 @@
+
+
+default_app_config = 'proxy.trac.apps.ProxyTracAppConfig'
diff --git a/colab/proxy/trac/admin.py b/colab/proxy/trac/admin.py
new file mode 100644
index 0000000..6127dc7
--- /dev/null
+++ b/colab/proxy/trac/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+from . import signals
diff --git a/colab/proxy/trac/apps.py b/colab/proxy/trac/apps.py
new file mode 100644
index 0000000..371c253
--- /dev/null
+++ b/colab/proxy/trac/apps.py
@@ -0,0 +1,7 @@
+
+from ..utils.apps import ColabProxiedAppConfig
+
+
+class ProxyTracAppConfig(ColabProxiedAppConfig):
+ name = 'proxy.trac'
+ verbose_name = 'Trac Proxy'
diff --git a/colab/proxy/trac/diazo.xml b/colab/proxy/trac/diazo.xml
new file mode 100644
index 0000000..03ab404
--- /dev/null
+++ b/colab/proxy/trac/diazo.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/colab/proxy/trac/migrations/0001_initial.py b/colab/proxy/trac/migrations/0001_initial.py
new file mode 100644
index 0000000..20f94eb
--- /dev/null
+++ b/colab/proxy/trac/migrations/0001_initial.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations, connections
+
+
+def create_views(apps, schema_editor):
+ connection = connections['trac']
+
+ cursor = connection.cursor()
+
+ # revision_view
+ cursor.execute('''
+ CREATE OR REPLACE VIEW revision_view AS SELECT
+ revision.rev,
+ revision.author,
+ revision.message,
+ repository.value AS repository_name,
+ TIMESTAMP WITH TIME ZONE 'epoch' + (revision.time/1000000) * INTERVAL '1s' AS created,
+ CONCAT(revision.repos, '-', revision.rev) AS key
+ FROM revision
+ INNER JOIN repository ON(
+ repository.id = revision.repos
+ AND repository.name = 'name'
+ AND repository.value != ''
+ );
+ ''')
+
+ # attachment_view
+ cursor.execute('''
+ CREATE OR REPLACE VIEW attachment_view AS SELECT
+ CONCAT(attachment.type, '/' , attachment.id, '/', attachment.filename) AS url,
+ attachment.type AS used_by,
+ attachment.filename AS filename,
+ attachment.id as attach_id,
+ (SELECT LOWER(SUBSTRING(attachment.filename FROM '\.(\w+)$'))) AS mimetype,
+ attachment.author AS author,
+ attachment.description AS description,
+ attachment.size AS size,
+ TIMESTAMP WITH TIME ZONE 'epoch' + (attachment.time/1000000)* INTERVAL '1s' AS created
+ FROM attachment;
+ ''')
+
+ # wiki_view
+ cursor.execute('''
+ CREATE OR REPLACE VIEW wiki_view AS SELECT
+ wiki.name AS name,
+ (SELECT wiki2.text FROM wiki AS wiki2 WHERE wiki2.name = wiki.name
+ AND wiki2.version = MAX(wiki.version)) AS wiki_text,
+ (SELECT wiki3.author FROM wiki AS wiki3 WHERE wiki3.name = wiki.name
+ AND wiki3.version = 1) AS author,
+ string_agg(DISTINCT wiki.author, ', ') AS collaborators,
+ TIMESTAMP WITH TIME ZONE 'epoch' + (MIN(wiki.time)/1000000) * INTERVAL '1s' AS created,
+ TIMESTAMP WITH TIME ZONE 'epoch' + (MAX(wiki.time)/1000000) * INTERVAL '1s' AS modified,
+ (SELECT wiki4.author FROM wiki AS wiki4 WHERE wiki4.name = wiki.name
+ AND wiki4.version = MAX(wiki.version)) AS modified_by
+ FROM wiki
+ GROUP BY wiki.name;
+ ''')
+
+ # ticket_view
+ cursor.execute('''
+ CREATE OR REPLACE VIEW ticket_view AS SELECT
+ ticket.id AS id,
+ ticket.summary as summary,
+ ticket.description as description,
+ ticket.milestone as milestone,
+ ticket.priority as priority,
+ ticket.component as component,
+ ticket.version as version,
+ ticket.severity as severity,
+ ticket.reporter as reporter,
+ ticket.reporter as author,
+ ticket.status as status,
+ ticket.keywords as keywords,
+ (SELECT
+ string_agg(DISTINCT ticket_change.author, ', ')
+ FROM ticket_change WHERE ticket_change.ticket = ticket.id
+ GROUP BY ticket_change.ticket) as collaborators,
+ TIMESTAMP WITH TIME ZONE 'epoch' + (time/1000000)* INTERVAL '1s' AS created,
+ TIMESTAMP WITH TIME ZONE 'epoch' + (changetime/1000000) * INTERVAL '1s' AS modified,
+ (SELECT
+ ticket_change.author
+ FROM ticket_change
+ WHERE ticket_change.ticket = ticket.id
+ AND ticket_change.time = ticket.changetime
+ LIMIT 1
+ ) AS modified_by
+ FROM ticket;
+ ''')
+
+ # ticket_collab_count_view
+ cursor.execute('''
+ CREATE OR REPLACE VIEW ticket_collab_count_view AS
+ SELECT
+ COALESCE (t1.author, t2.author) as author,
+ (COALESCE(t1.count, 0) + COALESCE(t2.count, 0)) as count
+ FROM
+ (SELECT author, count(*) as count
+ FROM ticket_change
+ GROUP BY author
+ ORDER BY author
+ ) AS t1
+ FULL OUTER JOIN
+ (SELECT reporter as author, count(*) as count
+ FROM ticket
+ GROUP BY reporter
+ ORDER BY reporter
+ ) AS t2
+ ON t1.author = t2.author;
+ ''')
+
+ # wiki_collab_count_view
+ cursor.execute('''
+ CREATE OR REPLACE VIEW wiki_collab_count_view AS
+ SELECT author, count(*) from wiki GROUP BY author;
+ ''')
+
+
+def drop_views(apps, schema_editor):
+ connection = connections['trac']
+
+ cursor = connection.cursor()
+ cursor.execute('''
+ DROP VIEW IF EXISTS revision_view;
+ DROP VIEW IF EXISTS ticket_view;
+ DROP VIEW IF EXISTS wiki_view;
+ DROP VIEW IF EXISTS ticket_collab_count_view;
+ DROP VIEW IF EXISTS wiki_collab_count_view;
+ DROP VIEW IF EXISTS attachment_view;
+ ''')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.RunPython(code=create_views, reverse_code=drop_views)
+ ]
diff --git a/colab/proxy/trac/migrations/__init__.py b/colab/proxy/trac/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/proxy/trac/migrations/__init__.py
diff --git a/colab/proxy/trac/models.py b/colab/proxy/trac/models.py
new file mode 100644
index 0000000..c71b74d
--- /dev/null
+++ b/colab/proxy/trac/models.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+from django.db import models
+from django.conf import settings
+import os
+import urllib2
+
+from accounts.models import User
+from hitcounter.models import HitCounterModelMixin
+
+
+class Attachment(models.Model, HitCounterModelMixin):
+ url = models.TextField(primary_key=True)
+ attach_id = models.TextField()
+ used_by = models.TextField()
+ filename = models.TextField()
+ author = models.TextField(blank=True)
+ description = models.TextField(blank=True)
+ created = models.DateTimeField(blank=True)
+ mimetype = models.TextField(blank=True)
+ size = models.IntegerField(blank=True)
+
+ class Meta:
+ managed = False
+ db_table = 'attachment_view'
+
+ @property
+ def filepath(self):
+ return os.path.join(
+ settings.ATTACHMENTS_FOLDER_PATH,
+ self.used_by,
+ self.attach_id,
+ urllib2.quote(self.filename.encode('utf8'))
+ )
+
+ def get_absolute_url(self):
+ return u'/raw-attachment/{}'.format(self.url)
+
+ def get_author(self):
+ try:
+ return User.objects.get(username=self.author)
+ except User.DoesNotExist:
+ return None
+
+
+class Revision(models.Model, HitCounterModelMixin):
+ key = models.TextField(blank=True, primary_key=True)
+ rev = models.TextField(blank=True)
+ author = models.TextField(blank=True)
+ message = models.TextField(blank=True)
+ repository_name = models.TextField(blank=True)
+ created = models.DateTimeField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = 'revision_view'
+
+ def get_absolute_url(self):
+ return u'/changeset/{}/{}'.format(self.rev, self.repository_name)
+
+ def get_author(self):
+ try:
+ return User.objects.get(username=self.author)
+ except User.DoesNotExist:
+ return None
+
+
+class Ticket(models.Model, HitCounterModelMixin):
+ id = models.IntegerField(primary_key=True)
+ summary = models.TextField(blank=True)
+ description = models.TextField(blank=True)
+ milestone = models.TextField(blank=True)
+ priority = models.TextField(blank=True)
+ component = models.TextField(blank=True)
+ version = models.TextField(blank=True)
+ severity = models.TextField(blank=True)
+ reporter = models.TextField(blank=True)
+ author = models.TextField(blank=True)
+ status = models.TextField(blank=True)
+ keywords = models.TextField(blank=True)
+ collaborators = models.TextField(blank=True)
+ created = models.DateTimeField(blank=True, null=True)
+ modified = models.DateTimeField(blank=True, null=True)
+ modified_by = models.TextField(blank=True)
+
+ class Meta:
+ managed = False
+ db_table = 'ticket_view'
+
+ def get_absolute_url(self):
+ return u'/ticket/{}'.format(self.id)
+
+ def get_author(self):
+ try:
+ return User.objects.get(username=self.author)
+ except User.DoesNotExist:
+ return None
+
+ def get_modified_by(self):
+ try:
+ return User.objects.get(username=self.modified_by)
+ except User.DoesNotExist:
+ return None
+
+
+class Wiki(models.Model, HitCounterModelMixin):
+ name = models.TextField(primary_key=True)
+ wiki_text = models.TextField(blank=True)
+ author = models.TextField(blank=True)
+ collaborators = models.TextField(blank=True)
+ created = models.DateTimeField(blank=True, null=True)
+ modified = models.DateTimeField(blank=True, null=True)
+ modified_by = models.TextField(blank=True)
+
+ class Meta:
+ managed = False
+ db_table = 'wiki_view'
+
+ def get_absolute_url(self):
+ return u'/wiki/{}'.format(self.name)
+
+ def get_author(self):
+ try:
+ return User.objects.get(username=self.author)
+ except User.DoesNotExist:
+ return None
+
+ def get_modified_by(self):
+ try:
+ return User.objects.get(username=self.modified_by)
+ except User.DoesNotExist:
+ return None
+
+
+class WikiCollabCount(models.Model):
+ author = models.TextField(primary_key=True)
+ count = models.IntegerField()
+
+ class Meta:
+ managed = False
+ db_table = 'wiki_collab_count_view'
+
+
+class TicketCollabCount(models.Model):
+ author = models.TextField(primary_key=True)
+ count = models.IntegerField()
+
+ class Meta:
+ managed = False
+ db_table = 'ticket_collab_count_view'
diff --git a/colab/proxy/trac/routers.py b/colab/proxy/trac/routers.py
new file mode 100644
index 0000000..9d053cb
--- /dev/null
+++ b/colab/proxy/trac/routers.py
@@ -0,0 +1,23 @@
+class TracRouter(object):
+ def db_for_read(self, model, **hints):
+ if model._meta.app_label == 'proxy':
+ return 'trac'
+ return None
+
+ def db_for_write(self, model, **hints):
+ if model._meta.app_label == 'proxy':
+ return 'trac'
+ return None
+
+ def allow_relation(self, obj1, obj2, **hints):
+ if obj1._meta.app_label == 'proxy' or \
+ obj2._meta.app_label == 'proxy':
+ return True
+ return None
+
+ def allow_migrate(self, db, model):
+ if db == 'trac':
+ return model._meta.app_label == 'proxy'
+ elif model._meta.app_label == 'proxy':
+ False
+ return None
diff --git a/colab/proxy/trac/search_indexes.py b/colab/proxy/trac/search_indexes.py
new file mode 100644
index 0000000..c7c413f
--- /dev/null
+++ b/colab/proxy/trac/search_indexes.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+
+import math
+import string
+
+from django.template import loader, Context
+from django.utils.text import slugify
+from haystack import indexes
+from haystack.utils import log as logging
+
+from search.base_indexes import BaseIndex
+from .models import Attachment, Ticket, Wiki, Revision
+
+
+logger = logging.getLogger('haystack')
+
+# the string maketrans always return a string encoded with latin1
+# http://stackoverflow.com/questions/1324067/how-do-i-get-str-translate-to-work-with-unicode-strings
+table = string.maketrans(
+ string.punctuation,
+ '.' * len(string.punctuation)
+).decode('latin1')
+
+
+class AttachmentIndex(BaseIndex, indexes.Indexable):
+ title = indexes.CharField(model_attr='filename')
+ description = indexes.CharField(model_attr='description', null=True)
+ modified = indexes.DateTimeField(model_attr='created', null=True)
+ used_by = indexes.CharField(model_attr='used_by', null=True, stored=False)
+ mimetype = indexes.CharField(
+ model_attr='mimetype',
+ null=True,
+ stored=False
+ )
+ size = indexes.IntegerField(model_attr='size', null=True, stored=False)
+ filename = indexes.CharField(stored=False)
+
+ def get_model(self):
+ return Attachment
+
+ def get_updated_field(self):
+ return 'created'
+
+ def prepare(self, obj):
+ data = super(AttachmentIndex, self).prepare(obj)
+
+ try:
+ file_obj = open(obj.filepath)
+ except IOError as e:
+ logger.warning(u'IOError: %s - %s', e.strerror, e.filename)
+ return data
+ backend = self._get_backend(None)
+
+ extracted_data = backend.extract_file_contents(file_obj)
+ file_obj.close()
+
+ if not extracted_data:
+ return data
+
+ t = loader.select_template(
+ ('search/indexes/proxy/attachment_text.txt', )
+ )
+ data['text'] = t.render(Context({
+ 'object': obj,
+ 'extracted': extracted_data,
+ }))
+ return data
+
+ def prepare_filename(self, obj):
+ return obj.filename.translate(table).replace('.', ' ')
+
+ def prepare_icon_name(self, obj):
+ return u'file'
+
+ def prepare_type(self, obj):
+ return u'attachment'
+
+
+class WikiIndex(BaseIndex, indexes.Indexable):
+ title = indexes.CharField(model_attr='name')
+ collaborators = indexes.CharField(
+ model_attr='collaborators',
+ null=True,
+ stored=False,
+ )
+
+ def get_model(self):
+ return Wiki
+
+ def prepare_description(self, obj):
+ return u'{}\n{}'.format(obj.wiki_text, obj.collaborators)
+
+ def prepare_icon_name(self, obj):
+ return u'book'
+
+ def prepare_type(self, obj):
+ return u'wiki'
+
+
+class TicketIndex(BaseIndex, indexes.Indexable):
+ tag = indexes.CharField(model_attr='status', null=True)
+ milestone = indexes.CharField(model_attr='milestone', null=True)
+ component = indexes.CharField(model_attr='component', null=True)
+ severity = indexes.CharField(model_attr='severity', null=True)
+ reporter = indexes.CharField(model_attr='reporter', null=True)
+ keywords = indexes.CharField(model_attr='keywords', null=True)
+ collaborators = indexes.CharField(
+ model_attr='collaborators',
+ null=True,
+ stored=False,
+ )
+
+ def get_model(self):
+ return Ticket
+
+ def prepare_description(self, obj):
+ return u'{}\n{}\n{}\n{}\n{}\n{}\n{}'.format(
+ obj.description, obj.milestone, obj.component, obj.severity,
+ obj.reporter, obj.keywords, obj.collaborators
+ )
+
+ def prepare_icon_name(self, obj):
+ return u'tag'
+
+ def prepare_title(self, obj):
+ return u'#{} - {}'.format(obj.pk, obj.summary)
+
+ def prepare_type(self, obj):
+ return 'ticket'
+
+
+class RevisionIndex(BaseIndex, indexes.Indexable):
+ description = indexes.CharField(model_attr='message', null=True)
+ modified = indexes.DateTimeField(model_attr='created', null=True)
+ repository_name = indexes.CharField(
+ model_attr='repository_name',
+ stored=False
+ )
+
+ def get_model(self):
+ return Revision
+
+ def get_updated_field(self):
+ return 'created'
+
+ def get_boost(self, obj):
+ boost = super(RevisionIndex, self).get_boost(obj)
+ return boost * 0.8
+
+ def prepare_icon_name(self, obj):
+ return u'align-right'
+
+ def prepare_title(self, obj):
+ return u'{} [{}]'.format(obj.repository_name, obj.rev)
+
+ def prepare_type(self, obj):
+ return 'changeset'
diff --git a/colab/proxy/trac/signals.py b/colab/proxy/trac/signals.py
new file mode 100644
index 0000000..0d477a6
--- /dev/null
+++ b/colab/proxy/trac/signals.py
@@ -0,0 +1,33 @@
+
+from django.db import connections
+from django.dispatch import receiver
+from django.db.models.signals import post_save
+
+from accounts.models import User
+
+
+@receiver(post_save, sender=User)
+def change_session_attribute_email(sender, instance, **kwargs):
+ cursor = connections['trac'].cursor()
+
+ cursor.execute(("UPDATE session_attribute SET value=%s "
+ "WHERE name='email' AND sid=%s"),
+ [instance.email, instance.username])
+ cursor.execute(("UPDATE session_attribute SET value=%s "
+ "WHERE name='name' AND sid=%s"),
+ [instance.get_full_name(), instance.username])
+
+ cursor.execute(("INSERT INTO session_attribute "
+ "(sid, authenticated, name, value) "
+ "SELECT %s, '1', 'email', %s WHERE NOT EXISTS "
+ "(SELECT 1 FROM session_attribute WHERE sid=%s "
+ "AND name='email')"),
+ [instance.username, instance.email, instance.username])
+
+ cursor.execute(("INSERT INTO session_attribute "
+ "(sid, authenticated, name, value) "
+ "SELECT %s, '1', 'name', %s WHERE NOT EXISTS "
+ "(SELECT 1 FROM session_attribute WHERE sid=%s "
+ "AND name='name')"),
+ [instance.username, instance.get_full_name(),
+ instance.username])
diff --git a/colab/proxy/trac/templates/proxy/trac.html b/colab/proxy/trac/templates/proxy/trac.html
new file mode 100644
index 0000000..bae56e4
--- /dev/null
+++ b/colab/proxy/trac/templates/proxy/trac.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block head %}
+
+
+ {{ block.super }}
+{% endblock %}
diff --git a/colab/proxy/trac/templates/search/indexes/proxy/attachment_text.txt b/colab/proxy/trac/templates/search/indexes/proxy/attachment_text.txt
new file mode 100644
index 0000000..9b22bae
--- /dev/null
+++ b/colab/proxy/trac/templates/search/indexes/proxy/attachment_text.txt
@@ -0,0 +1,15 @@
+{{ object.filename }}
+{{ object.filename|slugify }}
+{{ object.description }}
+{{ object.description|slugify }}
+{{ object.used_by }}
+{{ object.mimetype }}
+{{ object.get_author.get_full_name }}
+
+{% for k, v in extracted.metadata.items %}
+ {% for val in v %}
+ {{ k }}: {{ val|safe }}
+ {% endfor %}
+{% endfor %}
+
+{{ extracted.contents|striptags|safe }}
diff --git a/colab/proxy/trac/templates/search/indexes/proxy/revision_text.txt b/colab/proxy/trac/templates/search/indexes/proxy/revision_text.txt
new file mode 100644
index 0000000..64cca83
--- /dev/null
+++ b/colab/proxy/trac/templates/search/indexes/proxy/revision_text.txt
@@ -0,0 +1,8 @@
+{{ object.repository_name }}
+{{ object.repository_name|slugify }}
+{{ object.rev }}
+{{ object.rev|slugify }}
+{% firstof object.get_author.get_full_name object.author %}
+{% firstof object.get_author.get_full_name|slugify object.author|slugify %}
+{{ object.message }}
+{{ object.message|slugify }}
diff --git a/colab/proxy/trac/templates/search/indexes/proxy/ticket_text.txt b/colab/proxy/trac/templates/search/indexes/proxy/ticket_text.txt
new file mode 100644
index 0000000..93d199e
--- /dev/null
+++ b/colab/proxy/trac/templates/search/indexes/proxy/ticket_text.txt
@@ -0,0 +1,20 @@
+{{ object.summary }}
+{{ object.summary|slugify }}
+{{ object.description }}
+{{ object.description|slugify }}
+{{ object.milestone }}
+{{ object.milestone|slugify }}
+{{ object.component|slugify }}
+{{ object.version }}
+{{ object.severity }}
+{{ object.severity|slugify }}
+{{ object.reporter }}
+{{ object.reporter|slugify }}
+{% firstof object.get_author.get_fullname or object.author %}
+{% firstof object.get_author.get_fullname|slugify or object.author|slugify %}
+{{ object.status }}
+{{ object.status|slugify }}
+{{ object.keywords }}
+{{ object.keywords|slugify }}
+{{ object.collaborators }}
+{{ object.collaborators|slugify }}
diff --git a/colab/proxy/trac/templates/search/indexes/proxy/wiki_text.txt b/colab/proxy/trac/templates/search/indexes/proxy/wiki_text.txt
new file mode 100644
index 0000000..d1b51c4
--- /dev/null
+++ b/colab/proxy/trac/templates/search/indexes/proxy/wiki_text.txt
@@ -0,0 +1,9 @@
+{{ object.author }}
+{{ object.get_author.get_full_name }}
+{{ object.get_author.get_full_name|slugify }}
+{{ object.name }}
+{{ object.name|slugify }}
+{{ object.collaborators }}
+{{ object.collaborators|slugify }}
+{{ object.wiki_text }}
+{{ object.wiki_text|slugify }}
diff --git a/colab/proxy/trac/tests.py b/colab/proxy/trac/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/proxy/trac/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/proxy/trac/urls.py b/colab/proxy/trac/urls.py
new file mode 100644
index 0000000..1acbba2
--- /dev/null
+++ b/colab/proxy/trac/urls.py
@@ -0,0 +1,10 @@
+
+from django.conf.urls import patterns, url
+
+from .views import TracProxyView
+
+
+urlpatterns = patterns('',
+ # Trac
+ url(r'^(?P.*)$', TracProxyView.as_view()),
+)
diff --git a/colab/proxy/trac/views.py b/colab/proxy/trac/views.py
new file mode 100644
index 0000000..5ccdc85
--- /dev/null
+++ b/colab/proxy/trac/views.py
@@ -0,0 +1,42 @@
+
+from django.conf import settings
+
+from hitcounter.views import HitCounterViewMixin
+
+from ..utils.views import ColabProxyView
+from .models import Wiki, Ticket, Revision
+
+
+class TracProxyView(HitCounterViewMixin, ColabProxyView):
+ upstream = settings.PROXIED_APPS['trac']['upstream']
+ diazo_theme_template = 'proxy/trac.html'
+
+ def get_object(self):
+ obj = None
+
+ if self.request.path_info.startswith('/wiki'):
+ wiki_name = self.request.path_info.split('/', 2)[-1]
+ if not wiki_name:
+ wiki_name = 'WikiStart'
+ try:
+ obj = Wiki.objects.get(name=wiki_name)
+ except Wiki.DoesNotExist:
+ return None
+ elif self.request.path_info.startswith('/ticket'):
+ ticket_id = self.request.path_info.split('/')[2]
+ try:
+ obj = Ticket.objects.get(id=ticket_id)
+ except (Ticket.DoesNotExist, ValueError):
+ return None
+ elif self.request.path_info.startswith('/changeset'):
+ try:
+ changeset, repo = self.request.path_info.split('/')[2:4]
+ except ValueError:
+ return None
+ try:
+ obj = Revision.objects.get(rev=changeset,
+ repository_name=repo)
+ except Revision.DoesNotExist:
+ return None
+
+ return obj
diff --git a/colab/proxy/utils/__init__.py b/colab/proxy/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/proxy/utils/__init__.py
diff --git a/colab/proxy/utils/apps.py b/colab/proxy/utils/apps.py
new file mode 100644
index 0000000..9369ff1
--- /dev/null
+++ b/colab/proxy/utils/apps.py
@@ -0,0 +1,6 @@
+
+from django.apps import AppConfig
+
+
+class ColabProxiedAppConfig(AppConfig):
+ colab_proxied_app = True
diff --git a/colab/proxy/utils/views.py b/colab/proxy/utils/views.py
new file mode 100644
index 0000000..429ebe6
--- /dev/null
+++ b/colab/proxy/utils/views.py
@@ -0,0 +1,10 @@
+
+from django.conf import settings
+
+from revproxy.views import ProxyView
+
+
+class ColabProxyView(ProxyView):
+ add_remote_user = settings.REVPROXY_ADD_REMOTE_USER
+ diazo_theme_template = 'base.html'
+ html5 = True
diff --git a/colab/rss/__init__.py b/colab/rss/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/rss/__init__.py
diff --git a/colab/rss/feeds.py b/colab/rss/feeds.py
new file mode 100644
index 0000000..7bec55f
--- /dev/null
+++ b/colab/rss/feeds.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+from django.contrib.syndication.views import Feed
+from django.utils.translation import ugettext as _
+
+from haystack.query import SearchQuerySet
+
+from super_archives.models import Thread
+
+
+class LatestThreadsFeeds(Feed):
+ title = _(u'Latest Discussions')
+ link = '/rss/threads/latest/'
+
+ def items(self):
+ return Thread.objects.all()[:20]
+
+ def item_link(self, item):
+ return item.latest_message.url
+
+ def item_title(self, item):
+ title = '[' + item.mailinglist.name + '] '
+ title += item.latest_message.subject_clean
+ return title
+
+ def item_description(self, item):
+ return item.latest_message.body
+
+
+class HottestThreadsFeeds(Feed):
+ title = _(u'Discussions Most Relevance')
+ link = '/rss/threads/hottest/'
+
+ def items(self):
+ return Thread.highest_score.all()[:20]
+
+ def item_link(self, item):
+ return item.latest_message.url
+
+ def item_title(self, item):
+ title = '[' + item.mailinglist.name + '] '
+ title += item.latest_message.subject_clean
+ return title
+
+ def item_description(self, item):
+ return item.latest_message.body
+
+
+class LatestColabFeeds(Feed):
+ title = _(u'Latest collaborations')
+ link = '/rss/colab/latest/'
+
+ def items(self):
+ items = SearchQuerySet().order_by('-modified', '-created')[:20]
+ return items
+
+ def item_title(self, item):
+ type_ = item.type + ': '
+ mailinglist = item.tag
+
+ if mailinglist:
+ prefix = type_ + mailinglist + ' - '
+ else:
+ prefix = type_
+
+ return prefix + item.title
+
+ def item_description(self, item):
+ return item.latest_description
+
+ def item_link(self, item):
+ return item.url
diff --git a/colab/rss/urls.py b/colab/rss/urls.py
new file mode 100644
index 0000000..a26b6f6
--- /dev/null
+++ b/colab/rss/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls import patterns, url
+import feeds
+
+urlpatterns = patterns('',
+ url(r'threads/latest/$', feeds.LatestThreadsFeeds(), name='rss_latest_threads'),
+ url(r'colab/latest/$', feeds.LatestColabFeeds(), name='rss_latest_colab'),
+ url(r'threads/hottest/$', feeds.HottestThreadsFeeds(), name='rss_hottest_threads'),
+)
+
diff --git a/colab/search/__init__.py b/colab/search/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/colab/search/__init__.py
diff --git a/colab/search/admin.py b/colab/search/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/colab/search/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/colab/search/base_indexes.py b/colab/search/base_indexes.py
new file mode 100644
index 0000000..5a73182
--- /dev/null
+++ b/colab/search/base_indexes.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+
+import math
+
+from haystack import indexes
+
+
+class BaseIndex(indexes.SearchIndex):
+ text = indexes.CharField(document=True, use_template=True, stored=False)
+ url = indexes.CharField(model_attr='get_absolute_url', indexed=False)
+ title = indexes.CharField()
+ description = indexes.CharField(null=True)
+ fullname = indexes.CharField(null=True)
+ author = indexes.CharField(null=True)
+ author_url = indexes.CharField(null=True, indexed=False)
+ created = indexes.DateTimeField(model_attr='created', null=True)
+ modified = indexes.DateTimeField(model_attr='modified', null=True)
+ type = indexes.CharField()
+ icon_name = indexes.CharField(indexed=False)
+ fullname_and_username = indexes.CharField(null=True, stored=False)
+ hits = indexes.IntegerField(model_attr='hits')
+ modified_by = indexes.CharField(null=True)
+ modified_by_url = indexes.CharField(null=True)
+
+ def get_updated_field(self):
+ return 'modified'
+
+ def get_boost(self, obj):
+ if obj.hits <= 10:
+ return 1
+
+ return math.log(obj.hits)
+
+ def prepare(self, obj):
+ self.author_obj = None
+ if hasattr(obj, 'get_author'):
+ self.author_obj = obj.get_author()
+
+ data = super(BaseIndex, self).prepare(obj)
+ data['boost'] = self.get_boost(obj)
+
+ return data
+
+ def prepare_author(self, obj):
+ if self.author_obj:
+ return self.author_obj.username
+ return obj.author
+
+ def prepare_author_url(self, obj):
+ if self.author_obj:
+ return self.author_obj.get_absolute_url()
+ return None
+
+ def prepare_fullname(self, obj):
+ if hasattr(obj, 'modified_by'):
+ modified_by = obj.get_modified_by()
+ if modified_by:
+ return modified_by.get_full_name()
+ if self.author_obj:
+ return self.author_obj.get_full_name()
+ return obj.author
+
+ def prepare_fullname_and_username(self, obj):
+ if hasattr(obj, 'modified_by'):
+ modified_by = obj.get_modified_by()
+ if modified_by:
+ return u'{}\n{}'.format(
+ modified_by.get_full_name(),
+ modified_by.username,
+ )
+ if not self.author_obj:
+ return obj.author
+ return u'{}\n{}'.format(
+ self.author_obj.get_full_name(),
+ self.author_obj.username,
+ )
+
+ def prepare_modified_by(self, obj):
+ if hasattr(obj, 'modified_by'):
+ modified_by = obj.get_modified_by()
+ if modified_by:
+ return modified_by.get_full_name()
+ if self.author_obj:
+ return self.author_obj.get_full_name()
+ return obj.author
+
+ def prepare_modified_by_url(self, obj):
+ if hasattr(obj, 'modified_by'):
+ modified_by = obj.get_modified_by()
+ if modified_by:
+ return modified_by.get_absolute_url()
+ if self.author_obj:
+ return self.author_obj.get_absolute_url()
+ return None
diff --git a/colab/search/forms.py b/colab/search/forms.py
new file mode 100644
index 0000000..439286e
--- /dev/null
+++ b/colab/search/forms.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+
+import unicodedata
+
+from django import forms
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+from haystack.forms import SearchForm
+from haystack.inputs import AltParser
+
+from accounts.models import User
+from super_archives.models import Message, MailingList
+
+
+class ColabSearchForm(SearchForm):
+ q = forms.CharField(label=_('Search'), required=False)
+ order = forms.CharField(widget=forms.HiddenInput(), required=False)
+ type = forms.CharField(required=False, label=_(u'Type'))
+ author = forms.CharField(required=False, label=_(u'Author'))
+ modified_by = forms.CharField(required=False, label=_(u'Modified by'))
+ # ticket status
+ tag = forms.CharField(required=False, label=_(u'Status'))
+ # mailinglist tag
+ list = forms.MultipleChoiceField(
+ required=False,
+ label=_(u'Mailinglist'),
+ choices=[(v, v) for v in MailingList.objects.values_list(
+ 'name', flat=True)]
+ )
+ milestone = forms.CharField(required=False, label=_(u'Milestone'))
+ priority = forms.CharField(required=False, label=_(u'Priority'))
+ component = forms.CharField(required=False, label=_(u'Component'))
+ severity = forms.CharField(required=False, label=_(u'Severity'))
+ reporter = forms.CharField(required=False, label=_(u'Reporter'))
+ keywords = forms.CharField(required=False, label=_(u'Keywords'))
+ collaborators = forms.CharField(required=False, label=_(u'Collaborators'))
+ repository_name = forms.CharField(required=False, label=_(u'Repository'))
+ username = forms.CharField(required=False, label=_(u'Username'))
+ name = forms.CharField(required=False, label=_(u'Name'))
+ institution = forms.CharField(required=False, label=_(u'Institution'))
+ role = forms.CharField(required=False, label=_(u'Role'))
+ since = forms.DateField(required=False, label=_(u'Since'))
+ until = forms.DateField(required=False, label=_(u'Until'))
+ filename = forms.CharField(required=False, label=_(u'Filename'))
+ used_by = forms.CharField(required=False, label=_(u'Used by'))
+ mimetype = forms.CharField(required=False, label=_(u'File type'))
+ size = forms.CharField(required=False, label=_(u'Size'))
+
+ def search(self):
+ if not self.is_valid():
+ return self.no_query_found()
+
+ # filter_or goes here
+ sqs = self.searchqueryset.all()
+ mimetype = self.cleaned_data['mimetype']
+ if mimetype:
+ filter_mimetypes = {'mimetype__in': []}
+ for type_, display, mimelist in settings.FILE_TYPE_GROUPINGS:
+ if type_ in mimetype:
+ filter_mimetypes['mimetype__in'] += mimelist
+ if not self.cleaned_data['size']:
+ sqs = sqs.filter_or(mimetype__in=mimelist)
+
+ if self.cleaned_data['size']:
+ # (1024 * 1024) / 2
+ # (1024 * 1024) * 10
+ filter_sizes = {}
+ filter_sizes_exp = {}
+ if '<500KB' in self.cleaned_data['size']:
+ filter_sizes['size__lt'] = 524288
+ if '500KB__10MB' in self.cleaned_data['size']:
+ filter_sizes_exp['size__gte'] = 524288
+ filter_sizes_exp['size__lte'] = 10485760
+ if '>10MB' in self.cleaned_data['size']:
+ filter_sizes['size__gt'] = 10485760
+
+ if self.cleaned_data['mimetype']:
+ # Add the mimetypes filters to this dict and filter it
+ if filter_sizes_exp:
+ filter_sizes_exp.update(filter_mimetypes)
+ sqs = sqs.filter_or(**filter_sizes_exp)
+ for filter_or in filter_sizes.items():
+ filter_or = dict((filter_or, ))
+ filter_or.update(filter_mimetypes)
+ sqs = sqs.filter_or(**filter_or)
+ else:
+ for filter_or in filter_sizes.items():
+ filter_or = dict((filter_or, ))
+ sqs = sqs.filter_or(**filter_or)
+ sqs = sqs.filter_or(**filter_sizes_exp)
+
+ if self.cleaned_data['used_by']:
+ sqs = sqs.filter_or(used_by__in=self.cleaned_data['used_by'].split())
+
+
+ if self.cleaned_data['q']:
+ q = unicodedata.normalize(
+ 'NFKD', self.cleaned_data.get('q')
+ ).encode('ascii', 'ignore')
+
+ dismax_opts = {
+ 'q.alt': '*.*',
+ 'pf': 'title^2.1 author^1.9 description^1.7',
+ 'mm': '2<70%',
+
+ # Date boosting: http://wiki.apache.org/solr/FunctionQuery#Date_Boosting
+ 'bf': 'recip(ms(NOW/HOUR,modified),3.16e-11,1,1)^10',
+ }
+
+ sqs = sqs.filter(content=AltParser('edismax', q, **dismax_opts))
+
+ if self.cleaned_data['type']:
+ sqs = sqs.filter(type=self.cleaned_data['type'])
+
+ if self.cleaned_data['order']:
+ for option, dict_order in settings.ORDERING_DATA.items():
+ if self.cleaned_data['order'] == option:
+ if dict_order['fields']:
+ sqs = sqs.order_by(*dict_order['fields'])
+
+ if self.cleaned_data['author']:
+ sqs = sqs.filter(
+ fullname_and_username__contains=self.cleaned_data['author']
+ )
+
+ if self.cleaned_data['modified_by']:
+ sqs = sqs.filter(
+ fullname_and_username__contains=self.cleaned_data['modified_by']
+ )
+
+ if self.cleaned_data['milestone']:
+ sqs = sqs.filter(milestone=self.cleaned_data['milestone'])
+ if self.cleaned_data['priority']:
+ sqs = sqs.filter(priority=self.cleaned_data['priority'])
+ if self.cleaned_data['severity']:
+ sqs = sqs.filter(severity=self.cleaned_data['severity'])
+ if self.cleaned_data['reporter']:
+ sqs = sqs.filter(reporter=self.cleaned_data['reporter'])
+ if self.cleaned_data['keywords']:
+ sqs = sqs.filter(keywords=self.cleaned_data['keywords'])
+ if self.cleaned_data['collaborators']:
+ sqs = sqs.filter(collaborators=self.cleaned_data['collaborators'])
+ if self.cleaned_data['repository_name']:
+ sqs = sqs.filter(
+ repository_name=self.cleaned_data['repository_name']
+ )
+ if self.cleaned_data['username']:
+ sqs = sqs.filter(username=self.cleaned_data['username'])
+ if self.cleaned_data['name']:
+ sqs = sqs.filter(name=self.cleaned_data['name'])
+ if self.cleaned_data['institution']:
+ sqs = sqs.filter(institution=self.cleaned_data['institution'])
+ if self.cleaned_data['role']:
+ sqs = sqs.filter(role=self.cleaned_data['role'])
+ if self.cleaned_data['tag']:
+ sqs = sqs.filter(tag=self.cleaned_data['tag'])
+
+ if self.cleaned_data['list']:
+ sqs = sqs.filter(tag__in=self.cleaned_data['list'])
+
+ if self.cleaned_data['since']:
+ sqs = sqs.filter(modified__gte=self.cleaned_data['since'])
+ if self.cleaned_data['until']:
+ sqs = sqs.filter(modified__lte=self.cleaned_data['until'])
+
+ if self.cleaned_data['filename']:
+ sqs = sqs.filter(filename=self.cleaned_data['filename'])
+
+ return sqs
diff --git a/colab/search/models.py b/colab/search/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/colab/search/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/colab/search/templates/search/includes/search_filters.html b/colab/search/templates/search/includes/search_filters.html
new file mode 100644
index 0000000..d9068d7
--- /dev/null
+++ b/colab/search/templates/search/includes/search_filters.html
@@ -0,0 +1,201 @@
+{% load i18n superarchives %}
+
+{% if filters %}
+
+
+
+
+{% endif %}
+
+{% trans "Sort by" %}
+
+
+{% if not request.GET.type %}
+ {% trans "Types" %}
+
+
+{% endif %}
+
+
+
+
+
+
+
diff --git a/colab/search/templates/search/search-message-preview.html b/colab/search/templates/search/search-message-preview.html
new file mode 100644
index 0000000..e449429
--- /dev/null
+++ b/colab/search/templates/search/search-message-preview.html
@@ -0,0 +1,18 @@
+{% load i18n %}
+
+
+
+{% if result.mailinglist %}
+
+ {{ result.mailinglist }}
+
+{% endif %}
+
+
+
+ {{ result.title }}
+
+
+
+- {{ result.description|striptags }}
diff --git a/colab/search/templates/search/search-revision-preview.html b/colab/search/templates/search/search-revision-preview.html
new file mode 100644
index 0000000..1f99c44
--- /dev/null
+++ b/colab/search/templates/search/search-revision-preview.html
@@ -0,0 +1,9 @@
+{% load i18n %}
+
+
+
+
+ {{ result.repository_name }} [{{ result.revision }}]
+
+
+{% if result.message %}- {{ result.message }}{% endif %}
diff --git a/colab/search/templates/search/search-ticket-preview.html b/colab/search/templates/search/search-ticket-preview.html
new file mode 100644
index 0000000..eade62b
--- /dev/null
+++ b/colab/search/templates/search/search-ticket-preview.html
@@ -0,0 +1,16 @@
+{% load i18n %}
+{% load highlight %}
+
+
+
+
+{% if result.status %}
+ {{ result.status }}
+{% endif %}
+
+
+ #{{ result.pk }} - {% filter striptags|truncatewords:50 %}{{ result.summary|escape }}{% endfilter %}
+
+
+
+- {% highlight result.description with query max_length "150" %}
diff --git a/colab/search/templates/search/search-user-preview.html b/colab/search/templates/search/search-user-preview.html
new file mode 100644
index 0000000..cee56c5
--- /dev/null
+++ b/colab/search/templates/search/search-user-preview.html
@@ -0,0 +1,9 @@
+{% load i18n %}
+
+
+
+
+ {{ result.name }}
+
+
+{% if result.institution %}- {{ result.institution }}{% endif %}{% if result.role %} - {{ result.role }}{% endif %}
diff --git a/colab/search/templates/search/search-wiki-preview.html b/colab/search/templates/search/search-wiki-preview.html
new file mode 100644
index 0000000..e62f954
--- /dev/null
+++ b/colab/search/templates/search/search-wiki-preview.html
@@ -0,0 +1,9 @@
+{% load i18n %}
+
+
+
+
+ {{ result.name }}
+
+
+{% if result.wiki_text %}- {{ result.wiki_text|truncatechars:150 }}{% elif result.comment %}- {{ result.comment|truncatechars:150 }}{% endif %}
diff --git a/colab/search/templates/search/search.html b/colab/search/templates/search/search.html
new file mode 100644
index 0000000..a3b9ac2
--- /dev/null
+++ b/colab/search/templates/search/search.html
@@ -0,0 +1,159 @@
+{% extends "base.html" %}
+{% load i18n highlight superarchives static %}
+
+{% block title %}{% trans 'search'|title %}{% endblock %}
+
+{% block head_js %}
+
+{% if use_language %}
+
+{% endif %}
+
+
+{% endblock %}
+
+{% block head_css %}
+
+{% endblock %}
+
+{% block main-content %}
+
+
+
{% trans "Search" %}
+
+
+
+
+ {{ page.paginator.count }} {% trans "documents found" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Filters" %}
+ {% include "search/includes/search_filters.html" %}
+
+
+
+
+
+
+
+
+ {% include "search/includes/search_filters.html" %}
+
+
+
+
+
+
+
+
+
+
+ {% if page.has_other_pages %}
+
+
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/colab/search/tests.py b/colab/search/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/colab/search/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/colab/search/urls.py b/colab/search/urls.py
new file mode 100644
index 0000000..0139e86
--- /dev/null
+++ b/colab/search/urls.py
@@ -0,0 +1,14 @@
+from django.conf.urls import patterns, include, url
+from haystack.query import SearchQuerySet
+
+from .forms import ColabSearchForm
+from .views import ColabSearchView
+
+
+urlpatterns = patterns('',
+ url(r'^$', ColabSearchView(
+ template='search/search.html',
+ searchqueryset=SearchQuerySet(),
+ form_class=ColabSearchForm,
+ ), name='haystack_search'),
+)
diff --git a/colab/search/utils.py b/colab/search/utils.py
new file mode 100644
index 0000000..8372ee3
--- /dev/null
+++ b/colab/search/utils.py
@@ -0,0 +1,14 @@
+
+from django.utils.translation import ugettext as _
+
+
+def trans(key):
+ translations = {
+ 'wiki': _('Wiki'),
+ 'thread': _('Emails'),
+ 'changeset': _('Code'),
+ 'ticket': _('Tickets'),
+ 'attachment': _('Attachments'),
+ }
+
+ return translations.get(key, key)
diff --git a/colab/search/views.py b/colab/search/views.py
new file mode 100644
index 0000000..751f18e
--- /dev/null
+++ b/colab/search/views.py
@@ -0,0 +1,176 @@
+# -*- coding:utf-8 -*-
+
+from django.conf import settings
+from django.utils.translation import ugettext as _
+
+from haystack.views import SearchView
+
+#from proxy.trac.models import Attachment
+
+
+class ColabSearchView(SearchView):
+ def extra_context(self, *args, **kwargs):
+
+ use_language, date_format = settings.DJANGO_DATE_FORMAT_TO_JS.get(
+ self.request.LANGUAGE_CODE, (None, None)
+ )
+
+ types = {
+ 'thread': {
+ 'name': _(u'Discussion'),
+ 'fields': (
+ ('author', _(u'Author'), self.request.GET.get('author')),
+ (
+ 'list',
+ _(u'Mailinglist'),
+ self.request.GET.getlist('list')
+ ),
+ ),
+ },
+ }
+ # TODO: Replace for a more generic plugin architecture
+ #if settings.TRAC_ENABLED:
+ # types['wiki'] = {
+ # 'name': _(u'Wiki'),
+ # 'fields': (
+ # ('author', _(u'Author'), self.request.GET.get('author')),
+ # (
+ # 'collaborators',
+ # _(u'Collaborators'),
+ # self.request.GET.get('collaborators'),
+ # ),
+ # ),
+ # }
+
+ # types['ticket'] = {
+ # 'name': _(u'Ticket'),
+ # 'fields': (
+ # (
+ # 'milestone',
+ # _(u'Milestone'),
+ # self.request.GET.get('milestone')
+ # ),
+ # (
+ # 'priority',
+ # _(u'Priority'),
+ # self.request.GET.get('priority')
+ # ),
+ # (
+ # 'component',
+ # _(u'Component'),
+ # self.request.GET.get('component')
+ # ),
+ # (
+ # 'severity',
+ # _(u'Severity'),
+ # self.request.GET.get('severity')
+ # ),
+ # (
+ # 'reporter',
+ # _(u'Reporter'),
+ # self.request.GET.get('reporter')
+ # ),
+ # ('author', _(u'Author'), self.request.GET.get('author')),
+ # ('tag', _(u'Status'), self.request.GET.get('tag')),
+ # (
+ # 'keywords',
+ # _(u'Keywords'),
+ # self.request.GET.get('keywords'),
+ # ),
+ # (
+ # 'collaborators',
+ # _(u'Collaborators'),
+ # self.request.GET.get('collaborators')
+ # ),
+ # ),
+ # }
+
+ # types['changeset'] = {
+ # 'name': _(u'Changeset'),
+ # 'fields': (
+ # ('author', _(u'Author'), self.request.GET.get('author')),
+ # (
+ # 'repository_name',
+ # _(u'Repository'),
+ # self.request.GET.get('repository_name'),
+ # ),
+ # )
+ # }
+
+ # types['user'] = {
+ # 'name': _(u'User'),
+ # 'fields': (
+ # (
+ # 'username',
+ # _(u'Username'),
+ # self.request.GET.get('username'),
+ # ),
+ # ('name', _(u'Name'), self.request.GET.get('name')),
+ # (
+ # 'institution',
+ # _(u'Institution'),
+ # self.request.GET.get('institution'),
+ # ),
+ # ('role', _(u'Role'), self.request.GET.get('role'))
+ # ),
+ # }
+
+ # types['attachment'] = {
+ # 'name': _(u'Attachment'),
+ # 'fields': (
+ # (
+ # 'filename',
+ # _(u'Filename'),
+ # self.request.GET.get('filename')
+ # ),
+ # ('author', _(u'Author'), self.request.GET.get('author')),
+ # (
+ # 'used_by',
+ # _(u'Used by'), self.request.GET.get('used_by')),
+ # (
+ # 'mimetype',
+ # _(u'File type'),
+ # self.request.GET.get('mimetype')
+ # ),
+ # ('size', _(u'Size'), self.request.GET.get('size')),
+ # )
+ # }
+
+ try:
+ type_chosen = self.form.cleaned_data.get('type')
+ except AttributeError:
+ type_chosen = ''
+
+ mimetype_choices = ()
+ size_choices = ()
+ used_by_choices = ()
+
+ if type_chosen == 'attachment':
+ mimetype_choices = [(type_, display) for type_, display, mimelist_ in settings.FILE_TYPE_GROUPINGS]
+ size_choices = [
+ ('<500KB', u'< 500 KB'),
+ ('500KB__10MB', u'>= 500 KB <= 10 MB'),
+ ('>10MB', u'> 10 MB'),
+ ]
+ used_by_choices = set([
+ (v, v) for v in Attachment.objects.values_list(
+ 'used_by', flat=True)
+ ])
+
+ mimetype_chosen = self.request.GET.get('mimetype')
+ size_chosen = self.request.GET.get('size')
+ used_by_chosen = self.request.GET.get('used_by')
+
+ return dict(
+ filters=types.get(type_chosen),
+ type_chosen=type_chosen,
+ order_data=settings.ORDERING_DATA,
+ date_format=date_format,
+ use_language=use_language,
+ mimetype_chosen=mimetype_chosen if mimetype_chosen else '',
+ mimetype_choices=mimetype_choices,
+ size_chosen=size_chosen if size_chosen else '',
+ size_choices=size_choices,
+ used_by_chosen=used_by_chosen if used_by_chosen else '',
+ used_by_choices=used_by_choices,
+ )
diff --git a/colab/static/css/screen.css b/colab/static/css/screen.css
new file mode 100644
index 0000000..dc9cf89
--- /dev/null
+++ b/colab/static/css/screen.css
@@ -0,0 +1,451 @@
+
+body {
+ padding-top: 57px;
+}
+
+li hr {
+ margin: 10px 0;
+}
+
+/* Header */
+
+#header-searchbox {
+ width: 190px;
+}
+
+#header-hr {
+ margin-top: 0;
+}
+
+.navbar-default .navbar-brand,
+.navbar a.dropdown-toggle.user {
+ padding: 0;
+ margin-top: 5px;
+ margin-left: 10px;
+}
+
+.navbar-brand img {
+ height: 40px;
+}
+
+#user-menu .wrapper {
+ padding: 3px 10px;
+ white-space: nowrap;
+}
+
+#user-menu .wrapper a {
+ margin: 5px 0;
+}
+
+#user-menu .user-info {
+ display: inline-block;
+ vertical-align: top;
+ padding-left: 5px;
+}
+
+#user-menu .user-info span {
+ display: block;
+}
+
+#user-menu .dropdown-menu .thumbnail {
+ width: 100px;
+ display: inline-block;
+}
+
+/* End of Header */
+
+
+/* From message-preview.html*/
+.quiet {
+ color: #999;
+ font-size: 85%;
+}
+
+.preview-message {
+ white-space: nowrap;
+ overflow: hidden;
+ line-height: 25px;
+ border-bottom: solid 1px #ddd;
+}
+
+.preview-message a {
+ text-decoration: none;
+}
+
+.preview-message img {
+ margin-right: 5px;
+}
+
+.subject,
+.subject a {
+ color: #000;
+}
+
+.subject a:hover,
+.subject a:focus {
+ text-decoration: underline;
+ color: #335;
+}
+
+.subject img {
+ margin-right: 5px;
+}
+
+/* message-list (ul wrapping preview-message) */
+
+ul.message-list {
+ padding: 0;
+}
+
+ul.message-list li {
+ list-style: none;
+}
+
+/* End of message-preview.html */
+
+
+/* Forms */
+.required label:before {
+ color: #f00;
+ content: "* ";
+}
+
+form.signup .form-group {
+ width: 90%;
+ margin-left: 0.5em;
+}
+
+div.submit {
+ margin: auto;
+ margin-bottom: 5em;
+ width: 200px;
+}
+
+.checkbox ul {
+ list-style: none;
+ padding: 0;
+}
+
+.checkbox ul li {
+ line-height: 1.6;
+}
+
+/* End Forms */
+
+
+/* User profile */
+
+.vcard {
+ min-height: 400px;
+}
+
+.vcard .thumbnail {
+ width: 200px;
+ height: 200px;
+}
+
+.vcard h1 {
+ margin: 0;
+ padding: 0;
+ font-size: 26px;
+ line-height: 33px;
+ letter-spacing: -1px;
+}
+
+.vcard h1 span,
+.vcard h1 em {
+ display: inline-block;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ padding: 0;
+ vertical-align: bottom;
+}
+
+.vcard h1 em {
+ font-weight: 300;
+ font-size: 20px;
+ font-size: 20px;
+ letter-spacing: 1px;
+}
+
+.vcard .divider {
+ border-bottom: 1px solid #EEE;
+ margin-top: 10px;
+}
+
+
+/* end of User profile */
+
+
+.rss-icon {
+ background-image: url(../img/rss.png);
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ margin-left: 3px;
+}
+
+.column-align {
+ display: inline-block;
+ margin-bottom: 20px;
+}
+
+#filters h4 {
+ margin-top: 25px;
+}
+
+ul.unstyled-list {
+ list-style-type: none;
+ padding: 3px 0;
+ margin: 10px 0;
+}
+
+ul.unstyled-list li {
+ min-height: 27px;
+}
+
+ul.unstyled-list .glyphicon-chevron-right {
+ font-size: 85%;
+}
+
+#avatar {
+ background-color: #FFF;
+ padding: 9px 12px;
+ width: 47px;
+ height: 50px;
+}
+
+.blog-post-item .post-meta {
+ margin-bottom: 2em;
+}
+
+.blog-post-item .tags {
+ margin-top: 1em;
+}
+
+.reply.btn {
+ margin-left: 1em;
+}
+
+.email-message pre {
+ border: 0;
+ background-color: #fff;
+ font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+}
+
+.email-message .user-fullname {
+ vertical-align: middle;
+}
+
+.email-message .panel-heading img {
+ margin-right: 10px;
+}
+
+.email-message .panel-heading .date {
+ margin-right: 10px;
+}
+
+.email-message .panel-heading {
+ padding: 10px 0;
+}
+
+.selected .glyphicon-remove {
+ color: #f00;
+}
+
+.selected a {
+ color: #000;
+ font-weight: bold;
+}
+
+.toggle-reply {
+ height: 10px;
+ line-height: 3px;
+ padding: 0px 5px 14px;
+ vertical-align: middle;
+ font-size: 20px;
+ background-color: #bbb;
+ border-color: #aaa;
+}
+
+.toggle-reply:hover,
+.toggle-reply:focus {
+ background-color: #aaa;
+ border-color: #999;
+}
+
+/* Converse JS */
+
+#chatpanel form#converse-login {
+ padding: 0;
+}
+
+#chatpanel form#converse-login input,
+#chatpanel form#converse-login label {
+ margin: 2px 10px;
+}
+
+#chatpanel form#converse-login .login-submit{
+ margin-top: 10px;
+}
+
+#chatpanel a.configure-chatroom-button,
+#chatpanel a.close-chatbox-button {
+ background-color: #F6F6F6;
+ margin-right: 0.6em;
+}
+
+#chatpanel .chat-head #controlbox-tabs li {
+ width:32%;
+}
+
+#chatpanel .oc-chat-head {
+ height: 37px;
+}
+
+#chatpanel .chatbox dl.dropdown {
+ margin-top: 0;
+}
+
+#chatpanel #converse-roster {
+ height: 207px;
+}
+
+#chatpanel #toggle-controlbox {
+ font-size: 100% !important;
+ padding: 0 4px 26px !important;
+}
+
+#chatpanel {
+ height: 342px !important;
+}
+
+#chatpanel div#controlbox-panes {
+ width: auto;
+}
+
+#chatpanel .chat-head-chatbox, #chatpanel .chat-head-chatroom {
+ height: 40.5px;
+}
+
+#chatpanel form.sendXMPPMessage {
+ height: 76px;
+ border: none;
+}
+
+#chatpanel form.sendXMPPMessage textarea {
+ width: 189px;
+ max-width: 285px;
+ max-height: 66px;
+}
+
+#chatpanel .chatbox form.sendXMPPMessage textarea {
+ max-width: 189px;
+}
+
+#chatpanel .chat-content,
+#chatpanel .chatroom .participants{
+ width: auto;
+}
+
+#chatpanel div#chatrooms {
+ overflow-y: visible;
+}
+
+
+/* End of Converse JS*/
+
+
+/* Feedzilla */
+
+#planet .well h3 {
+ margin-top: 0;
+}
+
+.tag-cloud {
+ text-align: justify;
+ margin-top: 6px;
+}
+
+.tag-cloud .size-1 {
+ font-size: 0.9em;
+}
+
+.tag-cloud .size-2 {
+ font-size: 1.2em;
+}
+
+.tag-cloud .size-3 {
+ font-size: 1.4em;
+}
+
+.tag-cloud .size-4 {
+ font-size: 1.6em;
+}
+
+/* end Feedzilla */
+
+/* user profile update */
+
+.btn.delete-email:not(:hover) {
+ background-color: #fff;
+ color: #D9534F;
+}
+
+.btn .icon-warning-sign {
+ color: #F0AD4E;
+}
+
+ul.emails {
+ margin: 0;
+}
+
+/* end user profile update */
+
+/* search highlighting */
+span.highlighted {
+ background-color: yellow;
+}
+
+/* paginator icon */
+.small-icon {
+ font-size: 10px;
+}
+
+/* Subscribe list */
+.vcard .label {
+ line-height: 2;
+}
+
+/* Message link */
+
+.div-message-link {
+ display: inline !important;
+}
+
+.message-link {
+ margin-left: 15px;
+}
+
+.email-message .popover {
+ max-width: 350px;
+ width: 350px;
+}
+
+/* Chart div */
+.chart {
+ text-align: center;
+}
+
+.chart > canvas {
+ width: 250px;
+}
+
+.chart > p {
+ text-align: center;
+ line-height: 1.5em;
+}
+.chart > p > label {
+ margin: 0 2px;
+}
diff --git a/colab/static/dpaste/css/theme.css b/colab/static/dpaste/css/theme.css
new file mode 100644
index 0000000..5da06ca
--- /dev/null
+++ b/colab/static/dpaste/css/theme.css
@@ -0,0 +1,212 @@
+
+.shortcut {
+ color: #AAA;
+ font-size: 13px;
+ font-weight: 300;
+ margin-left: 15px;
+}
+
+.form-horizontal .form-group {
+ margin: 0 0 20px 0;
+}
+
+#id_content{
+ width: 100%;
+ font-family: monospace;
+ font-size: 14px;
+ line-height: 16px;
+}
+
+/* ----------------------------------------------------------------------------
+ Snippet Details
+---------------------------------------------------------------------------- */
+#snippet-diff {
+ display: none;
+}
+.snippet-options{
+ margin-bottom: 20px;
+}
+
+.snippet-reply {
+ margin-top: 30px;
+}
+
+.snippet-reply-hidden {
+ opacity: 0.3;
+}
+
+.snippet-reply-hidden,
+.snippet-reply-hidden *{
+ cursor: pointer;
+}
+
+.snippet-diff-form {
+}
+
+.snippet-rendered {
+ color: #666;
+ font-size: 16px;
+ line-height: 24px;
+ max-width: 620px;
+ font-family: Helvetica, FreeSerif, serif;
+ font-weight: 300;
+}
+.diff-form {
+/* margin-bottom: 10px;*/
+}
+
+#diff {
+ margin-bottom: 10px;
+/* display: none;*/
+}
+
+.tree{
+ width: 100%;
+ line-height: 1.8em;
+}
+
+.tree ul,
+.tree ul li{
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.tree ul li{
+ color: #ccc;
+ clear: both;
+}
+
+.tree ul li div{
+ border-bottom: 1px solid #EEE;
+}
+
+.tree strong{
+ color: #111;
+ font-weight: normal;
+}
+
+.tree ul li li{
+ padding-left: 0;
+ margin-left: 15px;
+ color: #ccc;
+ list-style: circle;
+}
+
+/* ----------------------------------------------------------------------------
+ .code
+---------------------------------------------------------------------------- */
+
+.code {
+ width: 100%;
+ background: #232829;
+ color: #f8f8f2;
+ padding: 20px 30px !important;
+ border-radius: 0;
+ padding: 20px 30px;
+ font-family: Monaco,Menlo,Consolas,"Courier New",monospace;
+}
+
+.code.wordwrap {
+ overflow: auto;
+ white-space: nowrap;
+}
+
+.code ol {
+ margin: 0 0 0 45px;
+}
+
+.code ol li {
+ color: #aaa;
+ font-size: 12px;
+ line-height: 21px;
+ cursor: pointer;
+ padding-left: 5px;
+}
+
+.code ol li.marked {
+ color: #f4e009;
+ background-color: #4f4800;
+ margin-right: -30px;
+ padding-right: 30px;
+}
+
+/* ----------------------------------------------------------------------------
+ Pygments
+---------------------------------------------------------------------------- */
+
+.code .gd { color: #FF494F; display: block; }
+.code .gi { color: #53C64A; display: block; }
+
+.code .hll { background-color: #49483e }
+.code .c { color: #75715e } /* Comment */
+.code .err { color: #960050; background-color: #1e0010 } /* Error */
+.code .k { color: #66d9ef } /* Keyword */
+.code .l { color: #ae81ff } /* Literal */
+.code .n { color: #f8f8f2 } /* Name */
+.code .o { color: #f92672 } /* Operator */
+.code .p { color: #f8f8f2 } /* Punctuation */
+.code .cm { color: #75715e } /* Comment.Multiline */
+.code .cp { color: #75715e } /* Comment..codeproc */
+.code .c1 { color: #75715e } /* Comment.Single */
+.code .cs { color: #75715e } /* Comment.Special */
+.code .ge { font-style: italic } /* Generic.Emph */
+.code .gs { font-weight: bold } /* Generic.Strong */
+.code .kc { color: #66d9ef } /* Keyword.Constant */
+.code .kd { color: #66d9ef } /* Keyword.Declaration */
+.code .kn { color: #f92672 } /* Keyword.Namespace */
+.code .kp { color: #66d9ef } /* Keyword.Pseudo */
+.code .kr { color: #66d9ef } /* Keyword.Reserved */
+.code .kt { color: #66d9ef } /* Keyword.Type */
+.code .ld { color: #e6db74 } /* Literal.Date */
+.code .m { color: #ae81ff } /* Literal.Number */
+.code .s { color: #e6db74 } /* Literal.String */
+.code .na { color: #a6e22e } /* Name.Attribute */
+.code .nb { color: #f8f8f2 } /* Name.Builtin */
+.code .nc { color: #a6e22e } /* Name.Class */
+.code .no { color: #66d9ef } /* Name.Constant */
+.code .nd { color: #a6e22e } /* Name.Decorator */
+.code .ni { color: #f8f8f2 } /* Name.Entity */
+.code .ne { color: #a6e22e } /* Name.Exception */
+.code .nf { color: #a6e22e } /* Name.Function */
+.code .nl { color: #f8f8f2 } /* Name.Label */
+.code .nn { color: #f8f8f2 } /* Name.Namespace */
+.code .nx { color: #a6e22e } /* Name.Other */
+.code .py { color: #f8f8f2 } /* Name.Property */
+.code .nt { color: #f92672 } /* Name.Tag */
+.code .nv { color: #f8f8f2 } /* Name.Variable */
+.code .ow { color: #f92672 } /* Operator.Word */
+.code .w { color: #f8f8f2 } /* Text.Whitespace */
+.code .mf { color: #ae81ff } /* Literal.Number.Float */
+.code .mh { color: #ae81ff } /* Literal.Number.Hex */
+.code .mi { color: #ae81ff } /* Literal.Number.Integer */
+.code .mo { color: #ae81ff } /* Literal.Number.Oct */
+.code .sb { color: #e6db74 } /* Literal.String.Backtick */
+.code .sc { color: #e6db74 } /* Literal.String.Char */
+.code .sd { color: #e6db74 } /* Literal.String.Doc */
+.code .s2 { color: #e6db74 } /* Literal.String.Double */
+.code .se { color: #ae81ff } /* Literal.String.Escape */
+.code .sh { color: #e6db74 } /* Literal.String.Heredoc */
+.code .si { color: #e6db74 } /* Literal.String.Interpol */
+.code .sx { color: #e6db74 } /* Literal.String.Other */
+.code .sr { color: #e6db74 } /* Literal.String.Regex */
+.code .s1 { color: #e6db74 } /* Literal.String.Single */
+.code .ss { color: #e6db74 } /* Literal.String.Symbol */
+.code .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
+.code .vc { color: #f8f8f2 } /* Name.Variable.Class */
+.code .vg { color: #f8f8f2 } /* Name.Variable.Global */
+.code .vi { color: #f8f8f2 } /* Name.Variable.Instance */
+.code .il { color: #ae81ff } /* Literal.Number.Integer.Long */
+
+
+/* ----------------------------------------------------------------------------
+ Mobile
+---------------------------------------------------------------------------- */
+
+@media (max-width: 580px) {
+ .form-options-expire {
+ float: left;
+ clear: left;
+ margin-top: 10px;
+ }
+}
diff --git a/colab/static/img/COPYRIGHT b/colab/static/img/COPYRIGHT
new file mode 100644
index 0000000..86cc332
--- /dev/null
+++ b/colab/static/img/COPYRIGHT
@@ -0,0 +1,11 @@
+The icons listed bellow were copied from the Iconic icons package. The icons in this set were originally designed for the Franklin Street WordPress theme and are available under CC Attribution-Share Alike 3.0 license - http://creativecommons.org/licenses/by-sa/3.0/us/
+
+* ticket.png
+* changeset.png
+* thread.png
+* wiki.png
+* x.png
+* plus.png
+* rss.png
+
+The full Iconic package can be found here: https://github.com/downloads/somerandomdude/Iconic/iconic.zip
diff --git a/colab/static/img/cc_by_sa.png b/colab/static/img/cc_by_sa.png
new file mode 100644
index 0000000..f70cb10
Binary files /dev/null and b/colab/static/img/cc_by_sa.png differ
diff --git a/colab/static/img/changeset.png b/colab/static/img/changeset.png
new file mode 100644
index 0000000..80e3ee1
Binary files /dev/null and b/colab/static/img/changeset.png differ
diff --git a/colab/static/img/fav.ico b/colab/static/img/fav.ico
new file mode 100644
index 0000000..4290246
Binary files /dev/null and b/colab/static/img/fav.ico differ
diff --git a/colab/static/img/logo.svg b/colab/static/img/logo.svg
new file mode 100644
index 0000000..cd22010
--- /dev/null
+++ b/colab/static/img/logo.svg
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/colab/static/img/opendata3.png b/colab/static/img/opendata3.png
new file mode 100644
index 0000000..dc12759
Binary files /dev/null and b/colab/static/img/opendata3.png differ
diff --git a/colab/static/img/plus.png b/colab/static/img/plus.png
new file mode 100644
index 0000000..16194ef
Binary files /dev/null and b/colab/static/img/plus.png differ
diff --git a/colab/static/img/rss.png b/colab/static/img/rss.png
new file mode 100644
index 0000000..978e758
Binary files /dev/null and b/colab/static/img/rss.png differ
diff --git a/colab/static/img/thread.png b/colab/static/img/thread.png
new file mode 100644
index 0000000..c429cbe
Binary files /dev/null and b/colab/static/img/thread.png differ
diff --git a/colab/static/img/ticket.png b/colab/static/img/ticket.png
new file mode 100644
index 0000000..09fcc0c
Binary files /dev/null and b/colab/static/img/ticket.png differ
diff --git a/colab/static/img/user.png b/colab/static/img/user.png
new file mode 100644
index 0000000..f027000
Binary files /dev/null and b/colab/static/img/user.png differ
diff --git a/colab/static/img/wiki.png b/colab/static/img/wiki.png
new file mode 100644
index 0000000..c267a33
Binary files /dev/null and b/colab/static/img/wiki.png differ
diff --git a/colab/static/img/x.png b/colab/static/img/x.png
new file mode 100644
index 0000000..0a766db
Binary files /dev/null and b/colab/static/img/x.png differ
diff --git a/colab/static/third-party/bootstrap-datetimepicker/README b/colab/static/third-party/bootstrap-datetimepicker/README
new file mode 100644
index 0000000..615774d
--- /dev/null
+++ b/colab/static/third-party/bootstrap-datetimepicker/README
@@ -0,0 +1,7 @@
+This bootstrap extension was copied from
+
+https://github.com/Eonasdan/bootstrap-datetimepicker
+ed42869337753e50adeb4a0e2476bfffc8edf2a9
+
+Distributed under Apache License Version 2.0:
+https://github.com/Eonasdan/bootstrap-datetimepicker/blob/master/LICENSE
diff --git a/colab/static/third-party/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css b/colab/static/third-party/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css
new file mode 100644
index 0000000..bc103a9
--- /dev/null
+++ b/colab/static/third-party/bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Datepicker for Bootstrap v3
+ *
+ * Copyright 2012 Stefan Petre
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+ .bootstrap-datetimepicker-widget{top:0;left:0;z-index:3000;width:250px;padding:4px;margin-top:1px;border-radius:4px}.bootstrap-datetimepicker-widget .btn{padding:6px}.bootstrap-datetimepicker-widget:before{position:absolute;top:-7px;left:6px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.bootstrap-datetimepicker-widget:after{position:absolute;top:-6px;left:7px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid white;border-left:6px solid transparent;content:''}.bootstrap-datetimepicker-widget.pull-right:before{right:6px;left:auto}.bootstrap-datetimepicker-widget.pull-right:after{right:7px;left:auto}.bootstrap-datetimepicker-widget>ul{margin:0;list-style-type:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:100%;font-size:1.2em;font-weight:bold}.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator{width:4px;padding:0;margin:0}.bootstrap-datetimepicker-widget .datepicker>div{display:none}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget td,.bootstrap-datetimepicker-widget th{width:20px;height:20px;text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget td.day:hover,.bootstrap-datetimepicker-widget td.hour:hover,.bootstrap-datetimepicker-widget td.minute:hover,.bootstrap-datetimepicker-widget td.second:hover{cursor:pointer;background:#eee}.bootstrap-datetimepicker-widget td.old,.bootstrap-datetimepicker-widget td.new{color:#999}.bootstrap-datetimepicker-widget td.active,.bootstrap-datetimepicker-widget td.active:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#428bca}.bootstrap-datetimepicker-widget td.disabled,.bootstrap-datetimepicker-widget td.disabled:hover{color:#999;cursor:not-allowed;background:0}.bootstrap-datetimepicker-widget td span{display:block;float:left;width:47px;height:54px;margin:2px;line-height:54px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget td span:hover{background:#eee}.bootstrap-datetimepicker-widget td span.active{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#428bca}.bootstrap-datetimepicker-widget td span.old{color:#999}.bootstrap-datetimepicker-widget td span.disabled,.bootstrap-datetimepicker-widget td span.disabled:hover{color:#999;cursor:not-allowed;background:0}.bootstrap-datetimepicker-widget th.switch{width:145px}.bootstrap-datetimepicker-widget th.next,.bootstrap-datetimepicker-widget th.prev{font-size:21px}.bootstrap-datetimepicker-widget th.disabled,.bootstrap-datetimepicker-widget th.disabled:hover{color:#999;cursor:not-allowed;background:0}.bootstrap-datetimepicker-widget thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget thead tr:first-child th:hover{background:#eee}.input-group.date .input-group-addon span{display:block;width:16px;height:16px;cursor:pointer}.bootstrap-datetimepicker-widget.left-oriented:before{right:6px;left:auto}.bootstrap-datetimepicker-widget.left-oriented:after{right:7px;left:auto}.bootstrap-datetimepicker-widget ul.list-unstyled li.in div.timepicker div.timepicker-picker table.table-condensed tbody>tr>td{padding:0!important}
\ No newline at end of file
diff --git a/colab/static/third-party/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js b/colab/static/third-party/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js
new file mode 100644
index 0000000..a9596d2
--- /dev/null
+++ b/colab/static/third-party/bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js
@@ -0,0 +1,28 @@
+/**
+ * version 1.0.6
+ * @license
+ * =========================================================
+ * bootstrap-datetimepicker.js
+ * http://www.eyecon.ro/bootstrap-datepicker
+ * =========================================================
+ * Copyright 2012 Stefan Petre
+ *
+ * Contributions:
+ * - Andrew Rowls
+ * - Thiago de Arruda
+ * - updated for Bootstrap v3 by Jonathan Peterson @Eonasdan
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * =========================================================
+ */
+(function(i){var q=(window.orientation!==undefined);var b=function(s,k){this.id=n++;this.init(s,k)};var o=function(k){if(typeof k==="string"){return new Date(k)}return k};b.prototype={constructor:b,init:function(s,k){var t=false;this.useMoment=(typeof moment!="undefined");if(!(k.pickTime||k.pickDate)){throw new Error("Must choose at least one picker")}this.options=k;this.$element=i(s);this.language=k.language in a?k.language:"en";this.pickDate=k.pickDate;this.pickTime=k.pickTime;this.isInput=this.$element.is("input");this.component=false;if(this.$element.hasClass("input-group")){this.component=this.$element.find(".input-group-addon")}this.format=k.format;if(!this.format){if(a[this.language].format!=null){this.format=a[this.language].format}else{if(this.isInput){this.format=this.$element.data("format")}else{this.format=this.$element.find("input").data("format")}}if(!this.format){this.format=(this.pickDate?"MM/dd/yyyy":"")}this.format+=(this.pickTime?" hh:mm":"")+(this.pickSeconds?":ss":"")}this._compileFormat();if(this.component){t=this.component.find("span")}if(this.pickTime){if(t&&t.length){this.timeIcon=t.data("time-icon");this.upIcon=t.data("up-icon");this.downIcon=t.data("down-icon")}if(!this.timeIcon){this.timeIcon="glyphicon glyphicon-time"}if(!this.upIcon){this.upIcon="glyphicon glyphicon-chevron-up"}if(!this.downIcon){this.downIcon="glyphicon glyphicon-chevron-down"}if(t){t.addClass(this.timeIcon)}}if(this.pickDate){if(t&&t.length){this.dateIcon=t.data("date-icon")}if(!this.dateIcon){this.dateIcon="glyphicon glyphicon-calendar"}if(t){t.removeClass(this.timeIcon);t.addClass(this.dateIcon)}}this.widget=i(d(this.timeIcon,this.upIcon,this.downIcon,k.pickDate,k.pickTime,k.pick12HourFormat,k.pickSeconds,k.collapse)).appendTo("body");this.minViewMode=k.minViewMode||this.$element.data("date-minviewmode")||0;if(typeof this.minViewMode==="string"){switch(this.minViewMode){case"months":this.minViewMode=1;break;case"years":this.minViewMode=2;break;default:this.minViewMode=0;break}}this.viewMode=k.viewMode||this.$element.data("date-viewmode")||0;if(typeof this.viewMode==="string"){switch(this.viewMode){case"months":this.viewMode=1;break;case"years":this.viewMode=2;break;default:this.viewMode=0;break}}if(k.defaultDate!==""){this.setValue(k.defaultDate)}this.startViewMode=this.viewMode;this.weekStart=k.weekStart||this.$element.data("date-weekstart")||0;this.weekEnd=this.weekStart===0?6:this.weekStart-1;setStartDate(k.startDate||this.$element.data("date-startdate"));this.setEndDate(k.endDate||this.$element.data("date-enddate"));this.fillDow();this.fillMonths();this.fillHours();this.fillMinutes();this.fillSeconds();this.update();this.showMode();this._attachDatePickerEvents()},show:function(k){this.widget.show();this.height=this.component?this.component.outerHeight():this.$element.outerHeight();this.place();this.$element.trigger({type:"show",date:this._date});this._attachDatePickerGlobalEvents();if(k){k.stopPropagation();k.preventDefault()}},disable:function(){this.$element.find("input").prop("disabled",true);this._detachDatePickerEvents()},enable:function(){this.$element.find("input").prop("disabled",false);this._attachDatePickerEvents()},hide:function(){var t=this.widget.find(".collapse");for(var k=0;ks.height()){t.top=t.top-(this.widget.height()+this.height+10)}if(this.options.width!==undefined){this.widget.width(this.options.width)}if(this.options.orientation==="left"){this.widget.addClass("left-oriented");t.left=t.left-this.widget.width()+20}if(this._isInFixed()){k="fixed";t.top-=s.scrollTop();t.left-=s.scrollLeft()}if(s.width()");while(k'+a[this.language].daysMin[(k++)%7]+"")}this.widget.find(".datepicker-days thead").append(s)},fillMonths:function(){var s="";var k=0;while(k<12){s+=''+a[this.language].monthsShort[k++]+" "}this.widget.find(".datepicker-months td").append(s)},fillDate:function(){var D=this.viewDate.getUTCFullYear();var B=this.viewDate.getUTCMonth();var s=m(this._date.getUTCFullYear(),this._date.getUTCMonth(),this._date.getUTCDate(),0,0,0,0);var C=typeof this.startDate==="object"?this.startDate.getUTCFullYear():-Infinity;var F=typeof this.startDate==="object"?this.startDate.getUTCMonth():-1;var G=typeof this.endDate==="object"?this.endDate.getUTCFullYear():Infinity;var z=typeof this.endDate==="object"?this.endDate.getUTCMonth():12;this.widget.find(".datepicker-days").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-months").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-years").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-days th:eq(1)").text(a[this.language].months[B]+" "+D);var v=m(D,B-1,28,0,0,0,0);var E=j.getDaysInMonth(v.getUTCFullYear(),v.getUTCMonth());v.setUTCDate(E);v.setUTCDate(E-(v.getUTCDay()-this.weekStart+7)%7);if((D==C&&B<=F)||D=z)||D>G){this.widget.find(".datepicker-days th:eq(2)").addClass("disabled")}var y=new Date(v.valueOf());y.setUTCDate(y.getUTCDate()+42);y=y.valueOf();var x=[];var H;var u;while(v.valueOf()");x.push(H)}u="";if(v.getUTCFullYear()D||(v.getUTCFullYear()==D&&v.getUTCMonth()>B)){u+=" new"}}if(v.valueOf()===s.valueOf()){u+=" active"}if((v.valueOf()+86400000)<=this.startDate){u+=" disabled"}if(v.valueOf()>this.endDate){u+=" disabled"}H.append(''+v.getUTCDate()+" ");v.setUTCDate(v.getUTCDate()+1)}this.widget.find(".datepicker-days tbody").empty().append(x);var A=this._date.getUTCFullYear();var k=this.widget.find(".datepicker-months").find("th:eq(1)").text(D).end().find("span").removeClass("active");if(A===D){k.eq(this._date.getUTCMonth()).addClass("active")}if(A-1G){this.widget.find(".datepicker-months th:eq(2)").addClass("disabled")}for(var w=0;w<12;w++){if((D==C&&F>w)||(DG)){i(k[w]).addClass("disabled")}}}x="";D=parseInt(D/10,10)*10;var t=this.widget.find(".datepicker-years").find("th:eq(1)").text(D+"-"+(D+9)).end().find("td");this.widget.find(".datepicker-years").find("th").removeClass("disabled");if(C>D){this.widget.find(".datepicker-years").find("th:eq(0)").addClass("disabled")}if(GG)?" disabled":"")+'">'+D+"";D+=1}t.html(x)},fillHours:function(){var u=this.widget.find(".timepicker .timepicker-hours table");u.parent().hide();var t="";if(this.options.pick12HourFormat){var v=1;for(var s=0;s<3;s+=1){t+="";for(var k=0;k<4;k+=1){var w=v.toString();t+=''+f(w,2,"0")+" ";v++}t+=" "}}else{var v=0;for(var s=0;s<6;s+=1){t+="";for(var k=0;k<4;k+=1){var w=v.toString();t+=''+f(w,2,"0")+" ";v++}t+=" "}}u.html(t)},fillMinutes:function(){var u=this.widget.find(".timepicker .timepicker-minutes table");u.parent().hide();var t="";var v=0;for(var s=0;s<5;s++){t+="";for(var k=0;k<4;k+=1){var w=v.toString();t+=''+f(w,2,"0")+" ";v+=3}t+=" "}u.html(t)},fillSeconds:function(){var u=this.widget.find(".timepicker .timepicker-seconds table");u.parent().hide();var t="";var v=0;for(var s=0;s<5;s++){t+="";for(var k=0;k<4;k+=1){var w=v.toString();t+=''+f(w,2,"0")+" ";v+=3}t+=" "}u.html(t)},fillTime:function(){if(!this._date){return}var x=this.widget.find(".timepicker span[data-time-component]");var u=x.closest("table");var t=this.options.pick12HourFormat;var k=this._date.getUTCHours();var w="AM";if(t){if(k>=12){w="PM"}if(k===0){k=12}else{if(k!=12){k=k%12}}this.widget.find(".timepicker [data-action=togglePeriod]").text(w)}k=f(k.toString(),2,"0");var v=f(this._date.getUTCMinutes().toString(),2,"0");var s=f(this._date.getUTCSeconds().toString(),2,"0");x.filter("[data-time-component=hours]").text(k);x.filter("[data-time-component=minutes]").text(v);x.filter("[data-time-component=seconds]").text(s)},click:function(y){y.stopPropagation();y.preventDefault();this._unset=false;var x=i(y.target).closest("span, td, th");if(x.length===1){if(!x.is(".disabled")){switch(x[0].nodeName.toLowerCase()){case"th":switch(x[0].className){case"switch":this.showMode(1);break;case"prev":case"next":var s=this.viewDate;var t=j.modes[this.viewMode].navFnc;var v=j.modes[this.viewMode].navStep;if(x[0].className==="prev"){v=v*-1}s["set"+t](s["get"+t]()+v);this.fillDate();break}break;case"span":if(x.is(".month")){var w=x.parent().find("span").index(x);this.viewDate.setUTCMonth(w)}else{var u=parseInt(x.text(),10)||0;this.viewDate.setUTCFullYear(u)}if(this.viewMode!==0){this._date=m(this.viewDate.getUTCFullYear(),this.viewDate.getUTCMonth(),this.viewDate.getUTCDate(),this._date.getUTCHours(),this._date.getUTCMinutes(),this._date.getUTCSeconds(),this._date.getUTCMilliseconds());this.notifyChange()}this.showMode(-1);this.fillDate();break;case"td":if(x.is(".day")){var k=parseInt(x.text(),10)||1;var w=this.viewDate.getUTCMonth();var u=this.viewDate.getUTCFullYear();if(x.is(".old")){if(w===0){w=11;u-=1}else{w-=1}}else{if(x.is(".new")){if(w==11){w=0;u+=1}else{w+=1}}}this._date=m(u,w,k,this._date.getUTCHours(),this._date.getUTCMinutes(),this._date.getUTCSeconds(),this._date.getUTCMilliseconds());this.viewDate=m(u,w,Math.min(28,k),0,0,0,0);this.fillDate();this.set();this.notifyChange()}break}}}},actions:{incrementHours:function(k){this._date.setUTCHours(this._date.getUTCHours()+1)},incrementMinutes:function(k){this._date.setUTCMinutes(this._date.getUTCMinutes()+1)},incrementSeconds:function(k){this._date.setUTCSeconds(this._date.getUTCSeconds()+1)},decrementHours:function(k){this._date.setUTCHours(this._date.getUTCHours()-1)},decrementMinutes:function(k){this._date.setUTCMinutes(this._date.getUTCMinutes()-1)},decrementSeconds:function(k){this._date.setUTCSeconds(this._date.getUTCSeconds()-1)},togglePeriod:function(s){var k=this._date.getUTCHours();if(k>=12){k-=12}else{k+=12}this._date.setUTCHours(k)},showPicker:function(){this.widget.find(".timepicker > div:not(.timepicker-picker)").hide();this.widget.find(".timepicker .timepicker-picker").show()},showHours:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-hours").show()},showMinutes:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-seconds").show()},selectHour:function(t){var u=i(t.target);var k=parseInt(u.text(),10);if(this.options.pick12HourFormat){var s=this._date.getUTCHours();if(s>=12){if(k!=12){k=(k+12)%24}}else{if(k===12){k=0}else{k=k%12}}}this._date.setUTCHours(k);this.actions.showPicker.call(this)},selectMinute:function(s){var t=i(s.target);var k=parseInt(t.text(),10);this._date.setUTCMinutes(k);this.actions.showPicker.call(this)},selectSecond:function(s){var t=i(s.target);var k=parseInt(t.text(),10);this._date.setUTCSeconds(k);this.actions.showPicker.call(this)}},doAction:function(s){s.stopPropagation();s.preventDefault();if(!this._date){this._date=m(1970,0,0,0,0,0,0)}var k=i(s.currentTarget).data("action");var t=this.actions[k].apply(this,arguments);this.set();this.fillTime();this.notifyChange();return t},stopEvent:function(k){k.stopPropagation();k.preventDefault()},keydown:function(v){var u=this,t=v.which,s=i(v.target);if(t==8||t==46){setTimeout(function(){u._resetMaskPos(s)})}},keypress:function(v){var u=v.which;if(u==8||u==46){return}var t=i(v.target);var x=String.fromCharCode(u);var w=t.val()||"";w+=x;var s=this._mask[this._maskPos];if(!s){return false}if(s.end!=w.length){return}if(!s.pattern.test(w.slice(s.start))){w=w.slice(0,w.length-1);while((s=this._mask[this._maskPos])&&s.character){w+=s.character;this._maskPos++}w+=x;if(s.end!=w.length){t.val(w);return false}else{if(!s.pattern.test(w.slice(s.start))){t.val(w.slice(0,s.start));return false}else{t.val(w);this._maskPos++;return false}}}else{this._maskPos++}},change:function(s){var k=i(s.target);var t=k.val();if(this._formatPattern.test(t)){this.update();this.setValue(this._date.getTime());this.notifyChange();this.set()}else{if(t&&t.trim()){this.setValue(this._date.getTime());if(this._date){this.set()}else{k.val("")}}else{if(this._date){this.setValue(null);this.notifyChange();this._unset=true}}}this._resetMaskPos(k)},showMode:function(k){if(k){this.viewMode=Math.max(this.minViewMode,Math.min(2,this.viewMode+k))}this.widget.find(".datepicker > div").hide().filter(".datepicker-"+j.modes[this.viewMode].clsName).show()},destroy:function(){this._detachDatePickerEvents();this._detachDatePickerGlobalEvents();this.widget.remove();this.$element.removeData("datetimepicker");this.component.removeData("datetimepicker")},formatDate:function(k){return this.format.replace(c,function(u){var t,v,w,s=u.length;if(u==="ms"){s=1}v=p[u].property;if(v==="Hours12"){w=k.getUTCHours();if(w===0){w=12}else{if(w!==12){w=w%12}}}else{if(v==="Period12"){if(k.getUTCHours()>=12){return"PM"}else{return"AM"}}else{t="get"+v;w=k[t]()}}if(t==="getUTCMonth"){w=w+1}if(t==="getUTCYear"){w=w+1900-2000}return f(w.toString(),s,"0")}).trim()},parseDate:function(y){if(this.useMoment){var x=moment(y);return x}var t,u,w,s,v,k={};if(!(t=this._formatPattern.exec(y))){return null}for(u=1;ut.length){this._maskPos=s;break}else{if(this._mask[s].end===t.length){this._maskPos=s+1;break}}}},_finishParsingDate:function(s){var w,x,u,k,v,y,t;w=s.UTCFullYear;if(s.UTCYear){w=2000+s.UTCYear}if(!w){w=1970}if(s.UTCMonth){x=s.UTCMonth-1}else{x=0}u=s.UTCDate||1;k=s.UTCHours||0;v=s.UTCMinutes||0;y=s.UTCSeconds||0;t=s.UTCMilliseconds||0;if(s.Hours12){k=s.Hours12}if(s.Period12){if(/pm/i.test(s.Period12)){if(k!=12){k=(k+12)%24}}else{k=k%12}}return m(w,x,u,k,v,y,t)},_compileFormat:function(){var u,t,w=[],s=[],x=this.format,k={},v=0,y=0;while(u=h.exec(x)){t=u[0];if(t in p){v++;k[v]=p[t].property;w.push("\\s*"+p[t].getPattern(this)+"\\s*");s.push({pattern:new RegExp(p[t].getPattern(this)),property:p[t].property,start:y,end:y+=t.length})}else{w.push(g(t));s.push({pattern:new RegExp(g(t)),character:t,start:y,end:++y})}x=x.slice(t.length)}this._mask=s;this._maskPos=0;this._formatPattern=new RegExp("^\\s*"+w.join("")+"\\s*$");this._propertiesByIndex=k},_attachDatePickerEvents:function(){var k=this;this.widget.on("click",".datepicker *",i.proxy(this.click,this));this.widget.on("click","[data-action]",i.proxy(this.doAction,this));this.widget.on("mousedown",i.proxy(this.stopEvent,this));if(this.pickDate&&this.pickTime){this.widget.on("click.togglePicker",".accordion-toggle",function(x){x.stopPropagation();var w=i(this);var v=w.closest("ul");var t=v.find(".in");var s=v.find(".collapse:not(.in)");if(t&&t.length){var u=t.data("collapse");if(u&&u.transitioning){return}t.collapse("hide");s.collapse("show");w.find("span").toggleClass(k.timeIcon+" "+k.dateIcon);k.$element.find(".input-group-addon span").toggleClass(k.timeIcon+" "+k.dateIcon)}})}if(this.isInput){this.$element.on({focus:i.proxy(this.show,this),change:i.proxy(this.change,this),blur:i.proxy(this.hide,this)});if(this.options.maskInput){this.$element.on({keydown:i.proxy(this.keydown,this),keypress:i.proxy(this.keypress,this)})}}else{this.$element.on({change:i.proxy(this.change,this)},"input");if(this.options.maskInput){this.$element.on({keydown:i.proxy(this.keydown,this),keypress:i.proxy(this.keypress,this)},"input")}if(this.component){this.component.on("click",i.proxy(this.show,this))}else{this.$element.on("click",i.proxy(this.show,this))}}},_attachDatePickerGlobalEvents:function(){i(window).on("resize.datetimepicker"+this.id,i.proxy(this.place,this));if(!this.isInput){i(document).on("mousedown.datetimepicker"+this.id,i.proxy(this.hide,this))}},_detachDatePickerEvents:function(){this.widget.off("click",".datepicker *",this.click);this.widget.off("click","[data-action]");this.widget.off("mousedown",this.stopEvent);if(this.pickDate&&this.pickTime){this.widget.off("click.togglePicker")}if(this.isInput){this.$element.off({focus:this.show,change:this.change});if(this.options.maskInput){this.$element.off({keydown:this.keydown,keypress:this.keypress})}}else{this.$element.off({change:this.change},"input");if(this.options.maskInput){this.$element.off({keydown:this.keydown,keypress:this.keypress},"input")}if(this.component){this.component.off("click",this.show)}else{this.$element.off("click",this.show)}}},_detachDatePickerGlobalEvents:function(){i(window).off("resize.datetimepicker"+this.id);if(!this.isInput){i(document).off("mousedown.datetimepicker"+this.id)}},_isInFixed:function(){if(this.$element){var s=this.$element.parents();var k=false;for(var t=0;t