Commit 8d6f24fc53f695213e2b6b8478d1508fbca16f02

Authored by Sergio Oliveira
2 parents b1879e4e 990d7321

Merge branch 'master' of github.com:TracyWebTech/colab

Showing 43 changed files with 1192 additions and 128 deletions   Show diff stats
requirements.txt
... ... @@ -8,6 +8,7 @@ chardet==1.0.1
8 8 python-dateutil==1.5
9 9 django-cliauth==0.9
10 10 django-mobile==0.3.0
  11 +django-haystack==2.1
11 12 etiquetando==0.1
12 13 html2text
13 14 django-taggit
... ...
src/accounts/search_indexes.py 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +from haystack import indexes
  4 +
  5 +from .models import User
  6 +
  7 +
  8 +class UserIndex(indexes.SearchIndex, indexes.Indexable):
  9 + # common fields
  10 + text = indexes.CharField(document=True, use_template=True)
  11 + url = indexes.CharField(model_attr='get_absolute_url')
  12 + title = indexes.CharField(model_attr='get_full_name')
  13 + description = indexes.CharField(null=True)
  14 + type = indexes.CharField()
  15 + icon_name = indexes.CharField()
  16 +
  17 + # extra fields
  18 + username = indexes.CharField(model_attr='username')
  19 + name = indexes.CharField(model_attr='get_full_name')
  20 + email = indexes.CharField(model_attr='email')
  21 + institution = indexes.CharField(model_attr='institution', null=True)
  22 + role = indexes.CharField(model_attr='role', null=True)
  23 + google_talk = indexes.CharField(model_attr='google_talk', null=True)
  24 + webpage = indexes.CharField(model_attr='webpage', null=True)
  25 +
  26 + def get_model(self):
  27 + return User
  28 +
  29 + def get_updated_field(self):
  30 + return 'date_joined'
  31 +
  32 + def prepare_description(self, obj):
  33 + return u'{}\n{}\n{}\n{}\n{}\n{}'.format(
  34 + obj.institution, obj.role, obj.username, obj.get_full_name(),
  35 + obj.google_talk, obj.webpage
  36 + )
  37 +
  38 + def prepare_icon_name(self, obj):
  39 + return u'user'
  40 +
  41 + def prepare_type(self, obj):
  42 + return u'user'
  43 +
  44 + def index_queryset(self, using=None):
  45 + return self.get_model().objects.filter(is_active=True)
... ...
src/accounts/templates/search/indexes/accounts/user_text.txt 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +{{ object.username }}
  2 +{{ object.get_full_name }}
  3 +{{ object.get_full_name|slugify }}
  4 +{{ object.institution }}
  5 +{{ object.institution|slugify }}
  6 +{{ object.role }}
  7 +{{ object.role|slugify }}
... ...
src/colab/custom_settings.py
... ... @@ -15,6 +15,56 @@ LANGUAGES = (
15 15  
16 16 LANGUAGE_CODE = 'pt-br'
17 17  
  18 +# ORDERING_DATA receives the options to order for as it's keys and a dict as
  19 +# value, if you want to order for the last name, you can use something like:
  20 +# 'last_name': {'name': 'Last Name', 'fields': 'last_name'} inside the dict,
  21 +# you pass two major keys (name, fields)
  22 +# The major key name is the name to appear on the template
  23 +# the major key fields it show receive the name of the fields to order for in
  24 +# the indexes
  25 +
  26 +ORDERING_DATA = {
  27 + 'latest': {
  28 + 'name': gettext(u'Recent activity'),
  29 + 'fields': ('-modified', '-created'),
  30 + },
  31 + 'hottest': {
  32 + 'name': gettext(u'Relevance'),
  33 + 'fields': None,
  34 + },
  35 +}
  36 +
  37 +# the following variable define how many characters should be shown before
  38 +# a highlighted word, to make sure that the highlighted word will appear
  39 +HIGHLIGHT_NUM_CHARS_BEFORE_MATCH = 30
  40 +HAYSTACK_CUSTOM_HIGHLIGHTER = 'colab.utils.highlighting.ColabHighlighter'
  41 +
  42 +HAYSTACK_CONNECTIONS = {
  43 + 'default': {
  44 + 'ENGINE': 'haystack.backends.solr_backend.SolrEngine',
  45 + 'URL': os.environ.get('COLAB_SOLR_URL'),
  46 + }
  47 +}
  48 +
  49 +DATABASES = {
  50 + 'default': {
  51 + 'ENGINE': 'django.db.backends.postgresql_psycopg2',
  52 + 'NAME': 'colab',
  53 + 'USER': 'colab',
  54 + 'PASSWORD': os.environ.get('COLAB_DEFAULT_DB_PWD'),
  55 + 'HOST': os.environ.get('COLAB_DEFAULT_DB_HOST'),
  56 + },
  57 + 'trac': {
  58 + 'ENGINE': 'django.db.backends.postgresql_psycopg2',
  59 + 'NAME': 'trac',
  60 + 'USER': 'trac',
  61 + 'PASSWORD': os.environ.get('COLAB_TRAC_DB_PWD'),
  62 + 'HOST': os.environ.get('COLAB_TRAC_DB_HOST'),
  63 + }
  64 +}
  65 +
  66 +DATABASE_ROUTERS = ['colab.routers.TracRouter',]
  67 +
18 68 INSTALLED_APPS = INSTALLED_APPS + (
19 69  
20 70 # Not standard apps
... ... @@ -24,6 +74,7 @@ INSTALLED_APPS = INSTALLED_APPS + (
24 74 'django_mobile',
25 75 'django_browserid',
26 76 'conversejs',
  77 + 'haystack',
27 78  
28 79 # Own apps
29 80 'super_archives',
... ... @@ -33,6 +84,7 @@ INSTALLED_APPS = INSTALLED_APPS + (
33 84 'planet',
34 85 'accounts',
35 86 'proxy',
  87 + 'search',
36 88  
37 89 # Feedzilla and deps
38 90 'feedzilla',
... ...
src/colab/deprecated/views/other.py
... ... @@ -6,12 +6,16 @@ other.py
6 6 Created by Sergio Campos on 2012-01-10.
7 7 """
8 8  
  9 +import datetime
  10 +
9 11 from django.template import RequestContext
10 12 from django.http import HttpResponseNotAllowed
11 13 from django.shortcuts import render_to_response
  14 +from django.utils import timezone
12 15 from django.utils.translation import ugettext as _
13 16  
14   -from colab.deprecated import solrutils
  17 +from haystack.query import SearchQuerySet
  18 +
15 19 from super_archives import queries
16 20  
17 21  
... ... @@ -21,11 +25,21 @@ def home(request):
21 25 latest_threads = queries.get_latest_threads()
22 26 hottest_threads = queries.get_hottest_threads()
23 27  
  28 + count_types = {}
  29 + six_months = timezone.now() - datetime.timedelta(days=180)
  30 + for type in ['wiki', 'thread', 'changeset', 'ticket']:
  31 + count_types[type] = SearchQuerySet().filter(
  32 + type=type,
  33 + modified__gte=six_months,
  34 + ).count()
  35 +
24 36 template_data = {
25 37 'hottest_threads': hottest_threads[:6],
26 38 'latest_threads': latest_threads[:6],
27   - 'type_count': solrutils.count_types(sample=1000),
28   - 'latest_docs': solrutils.get_latest_collaborations(6),
  39 + 'type_count': count_types,
  40 + 'latest_results': SearchQuerySet().all().order_by(
  41 + '-modified', '-created'
  42 + )[:6],
29 43 }
30 44 return render_to_response('home.html', template_data,
31 45 context_instance=RequestContext(request))
... ... @@ -34,7 +48,7 @@ def home(request):
34 48 def search(request):
35 49 if request.method != 'GET':
36 50 return HttpResponseNotAllowed(['GET'])
37   -
  51 +
38 52 query = request.GET.get('q')
39 53 sort = request.GET.get('o')
40 54 type_ = request.GET.get('type')
... ... @@ -42,7 +56,7 @@ def search(request):
42 56 page_number = int(request.GET.get('p', '1'))
43 57 except ValueError:
44 58 page_number = 1
45   -
  59 +
46 60 try:
47 61 results_per_page = int(request.GET.get('per_page', 16))
48 62 except ValueError:
... ...
src/colab/routers.py 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +class TracRouter(object):
  2 + def db_for_read(self, model, **hints):
  3 + if model._meta.app_label == 'proxy':
  4 + return 'trac'
  5 + return None
  6 +
  7 + def db_for_write(self, model, **hints):
  8 + if model._meta.app_label == 'proxy':
  9 + return 'trac'
  10 + return None
  11 +
  12 + def allow_relation(self, obj1, obj2, **hints):
  13 + if obj1._meta.app_label == 'proxy' or \
  14 + obj2._meta.app_label == 'proxy':
  15 + return True
  16 + return None
  17 +
  18 + def allow_migrate(self, db, model):
  19 + if db == 'trac':
  20 + return model._meta.app_label == 'proxy'
  21 + elif model._meta.app_label == 'proxy':
  22 + False
  23 + return None
... ...
src/colab/urls.py
1   -
2 1 from django.conf.urls import patterns, include, url
3 2 from django.conf import settings
4 3 from django.views.generic import TemplateView
5 4 from django.contrib import admin
6 5  
  6 +from accounts.models import User
  7 +from search.forms import ColabSearchForm
  8 +from super_archives.models import Message
  9 +
7 10  
8 11 admin.autodiscover()
9 12  
10 13 urlpatterns = patterns('',
11 14 url(r'^$', 'colab.deprecated.views.other.home', name='home'),
12 15  
13   - url(r'^search/$', 'colab.deprecated.views.other.search', name='search'),
14   -
  16 + url(r'^search/', include('search.urls')),
15 17 url(r'open-data/$', TemplateView.as_view(template_name='open-data.html'),
16 18 name='opendata'),
17 19  
... ...
src/colab/utils/__init__.py 0 → 100644
src/colab/utils/highlighting.py 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +from haystack.utils import Highlighter
  2 +from django.conf import settings
  3 +
  4 +
  5 +class ColabHighlighter(Highlighter):
  6 + def find_window(self, highlight_locations):
  7 + """Getting the HIGHLIGHT_NUM_CHARS_BEFORE_MATCH setting
  8 + to find how many characters before the first word found should
  9 + be removed from the window
  10 + """
  11 +
  12 + if len(self.text_block) <= self.max_length:
  13 + return (0, self.max_length)
  14 +
  15 + num_chars_before = getattr(
  16 + settings,
  17 + 'HIGHLIGHT_NUM_CHARS_BEFORE_MATCH',
  18 + 0
  19 + )
  20 +
  21 + best_start, best_end = super(ColabHighlighter, self).find_window(
  22 + highlight_locations
  23 + )
  24 + if best_start <= num_chars_before:
  25 + best_end -= best_start
  26 + best_start = 0
  27 + else:
  28 + best_start -= num_chars_before
  29 + best_end -= num_chars_before
  30 +
  31 + return (best_start, best_end)
... ...
src/proxy/migrations/0001_create_views.py 0 → 100644
... ... @@ -0,0 +1,115 @@
  1 +# -*- coding: utf-8 -*-
  2 +import datetime
  3 +from django.db import connections
  4 +from south.db import db
  5 +from south.v2 import DataMigration
  6 +from django.db import models
  7 +
  8 +
  9 +class Migration(DataMigration):
  10 +
  11 + def forwards(self, orm):
  12 + # Selecting trac database
  13 + connection = connections['trac']
  14 +
  15 + cursor = connection.cursor()
  16 + cursor.execute('''
  17 + CREATE OR REPLACE VIEW wiki_view AS SELECT
  18 + wiki.name AS name,
  19 + (SELECT wiki2.text FROM wiki AS wiki2 WHERE wiki2.name = wiki.name
  20 + AND wiki2.version = MAX(wiki.version)) AS wiki_text,
  21 + (SELECT wiki3.author FROM wiki AS wiki3 WHERE wiki3.name = wiki.name
  22 + AND wiki3.version = 1) AS author,
  23 + string_agg(DISTINCT wiki.author, ', ') AS collaborators,
  24 + TIMESTAMP WITH TIME ZONE 'epoch' + (MAX(wiki.time)/1000000) * INTERVAL '1s' AS created,
  25 + TIMESTAMP WITH TIME ZONE 'epoch' + (MIN(wiki.time)/1000000) * INTERVAL '1s' AS modified
  26 + FROM wiki
  27 + GROUP BY wiki.name;
  28 +
  29 + CREATE OR REPLACE VIEW ticket_view AS SELECT
  30 + ticket.id AS id,
  31 + ticket.summary as summary,
  32 + ticket.description as description,
  33 + ticket.milestone as milestone,
  34 + ticket.priority as priority,
  35 + ticket.component as component,
  36 + ticket.version as version,
  37 + ticket.severity as severity,
  38 + ticket.reporter as reporter,
  39 + ticket.reporter as author,
  40 + ticket.status as status,
  41 + ticket.keywords as keywords,
  42 + (SELECT
  43 + string_agg(DISTINCT ticket_change.author, ', ')
  44 + FROM ticket_change WHERE ticket_change.ticket = ticket.id
  45 + GROUP BY ticket_change.ticket) as collaborators,
  46 + TIMESTAMP WITH TIME ZONE 'epoch' + (time/1000000)* INTERVAL '1s' AS created,
  47 + TIMESTAMP WITH TIME ZONE 'epoch' + (changetime/1000000) * INTERVAL '1s' AS modified
  48 + FROM ticket;
  49 +
  50 + CREATE OR REPLACE VIEW revision_view AS SELECT
  51 + revision.rev,
  52 + revision.author,
  53 + revision.message,
  54 + repository.value AS repository_name,
  55 + TIMESTAMP WITH TIME ZONE 'epoch' + (revision.time/1000000) * INTERVAL '1s' AS created
  56 + FROM revision
  57 + INNER JOIN repository ON(
  58 + repository.id = revision.repos
  59 + AND repository.name = 'name'
  60 + AND repository.value != ''
  61 + );
  62 + ''')
  63 +
  64 + def backwards(self, orm):
  65 + # Selecting trac database
  66 + connection = connections['trac']
  67 +
  68 + cursor = connection.cursor()
  69 + cursor.execute('''
  70 + DROP VIEW IF EXISTS revision_view;
  71 + DROP VIEW IF EXISTS ticket_view;
  72 + DROP VIEW IF EXISTS wiki_view;
  73 + ''')
  74 +
  75 +
  76 + models = {
  77 + u'proxy.revision': {
  78 + 'Meta': {'object_name': 'Revision', 'db_table': "'revision_view'", 'managed': 'False'},
  79 + 'author': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  80 + 'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
  81 + 'message': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  82 + 'repository_name': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  83 + 'rev': ('django.db.models.fields.TextField', [], {'primary_key': 'True'})
  84 + },
  85 + u'proxy.ticket': {
  86 + 'Meta': {'object_name': 'Ticket', 'db_table': "'ticket_view'", 'managed': 'False'},
  87 + 'author': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  88 + 'collaborators': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  89 + 'component': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  90 + 'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
  91 + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  92 + 'id': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}),
  93 + 'keywords': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  94 + 'milestone': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  95 + 'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
  96 + 'priority': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  97 + 'reporter': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  98 + 'severity': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  99 + 'status': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  100 + 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  101 + 'version': ('django.db.models.fields.TextField', [], {'blank': 'True'})
  102 + },
  103 + u'proxy.wiki': {
  104 + 'Meta': {'object_name': 'Wiki', 'db_table': "'wiki_view'", 'managed': 'False'},
  105 + 'author': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  106 + 'collaborators': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
  107 + 'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
  108 + 'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
  109 + 'name': ('django.db.models.fields.TextField', [], {'primary_key': 'True'}),
  110 + 'wiki_text': ('django.db.models.fields.TextField', [], {'blank': 'True'})
  111 + }
  112 + }
  113 +
  114 + complete_apps = ['proxy']
  115 + symmetrical = True
... ...
src/proxy/migrations/__init__.py 0 → 100644
src/proxy/models.py
  1 +# -*- coding: utf-8 -*-
  2 +
1 3 from django.db import models
2 4  
3   -# Create your models here.
  5 +from accounts.models import User
  6 +
  7 +
  8 +# get_absolute_url em todos
  9 +# get_author_url em todos
  10 +
  11 +
  12 +class Revision(models.Model):
  13 + rev = models.TextField(blank=True, primary_key=True)
  14 + author = models.TextField(blank=True)
  15 + message = models.TextField(blank=True)
  16 + repository_name = models.TextField(blank=True)
  17 + created = models.DateTimeField(blank=True, null=True)
  18 +
  19 + class Meta:
  20 + managed = False
  21 + db_table = 'revision_view'
  22 +
  23 + def get_absolute_url(self):
  24 + return u'/changeset/{}/{}'.format(self.rev, self.repository_name)
  25 +
  26 + def get_author(self):
  27 + try:
  28 + return User.objects.get(username=self.author)
  29 + except User.DoesNotExist:
  30 + return None
  31 +
  32 +class Ticket(models.Model):
  33 + id = models.IntegerField(primary_key=True)
  34 + summary = models.TextField(blank=True)
  35 + description = models.TextField(blank=True)
  36 + milestone = models.TextField(blank=True)
  37 + priority = models.TextField(blank=True)
  38 + component = models.TextField(blank=True)
  39 + version = models.TextField(blank=True)
  40 + severity = models.TextField(blank=True)
  41 + reporter = models.TextField(blank=True)
  42 + author = models.TextField(blank=True)
  43 + status = models.TextField(blank=True)
  44 + keywords = models.TextField(blank=True)
  45 + collaborators = models.TextField(blank=True)
  46 + created = models.DateTimeField(blank=True, null=True)
  47 + modified = models.DateTimeField(blank=True, null=True)
  48 +
  49 + class Meta:
  50 + managed = False
  51 + db_table = 'ticket_view'
  52 +
  53 + def get_absolute_url(self):
  54 + return u'/ticket/{}'.format(self.id)
  55 +
  56 + def get_author(self):
  57 + try:
  58 + return User.objects.get(username=self.author)
  59 + except User.DoesNotExist:
  60 + return None
  61 +
  62 +
  63 +class Wiki(models.Model):
  64 + name = models.TextField(primary_key=True)
  65 + wiki_text = models.TextField(blank=True)
  66 + author = models.TextField(blank=True)
  67 + collaborators = models.TextField(blank=True)
  68 + created = models.DateTimeField(blank=True, null=True)
  69 + modified = models.DateTimeField(blank=True, null=True)
  70 +
  71 + class Meta:
  72 + managed = False
  73 + db_table = 'wiki_view'
  74 +
  75 + def get_absolute_url(self):
  76 + return u'/ticket/{}'.format(self.name)
  77 +
  78 + def get_author(self):
  79 + try:
  80 + return User.objects.get(username=self.author)
  81 + except User.DoesNotExist:
  82 + return None
... ...
src/proxy/search_indexes.py 0 → 100644
... ... @@ -0,0 +1,153 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +from datetime import datetime
  4 +
  5 +from django.db.models import Q
  6 +from haystack import indexes
  7 +
  8 +from .models import Ticket, Wiki, Revision
  9 +
  10 +
  11 +class WikiIndex(indexes.SearchIndex, indexes.Indexable):
  12 + # common fields
  13 + text = indexes.CharField(document=True, use_template=True)
  14 + url = indexes.CharField(model_attr='get_absolute_url')
  15 + title = indexes.CharField(model_attr='name')
  16 + description = indexes.CharField(null=True)
  17 + author = indexes.CharField(null=True)
  18 + author_url = indexes.CharField(null=True)
  19 + created = indexes.DateTimeField(model_attr='created', null=True)
  20 + modified = indexes.DateTimeField(model_attr='modified', null=True)
  21 + type = indexes.CharField()
  22 + icon_name = indexes.CharField()
  23 +
  24 + # trac extra fields
  25 + collaborators = indexes.CharField(model_attr='collaborators', null=True)
  26 +
  27 + def get_model(self):
  28 + return Wiki
  29 +
  30 + def get_updated_field(self):
  31 + return 'modified'
  32 +
  33 + def prepare_author(self, obj):
  34 + author = obj.get_author()
  35 + if author:
  36 + return author.get_full_name()
  37 + return obj.author
  38 +
  39 + def prepare_author_url(self, obj):
  40 + author = obj.get_author()
  41 + if author:
  42 + return author.get_absolute_url()
  43 + return None
  44 +
  45 + def prepare_description(self, obj):
  46 + return u'{}\n{}'.format(obj.wiki_text, obj.collaborators)
  47 +
  48 + def prepare_icon_name(self, obj):
  49 + return u'file'
  50 +
  51 + def prepare_type(self, obj):
  52 + return u'wiki'
  53 +
  54 +
  55 +class TicketIndex(indexes.SearchIndex, indexes.Indexable):
  56 + # common fields
  57 + text = indexes.CharField(document=True, use_template=True)
  58 + url = indexes.CharField(model_attr='get_absolute_url')
  59 + title = indexes.CharField()
  60 + description = indexes.CharField(null=True)
  61 + author = indexes.CharField(null=True)
  62 + author_url = indexes.CharField(null=True)
  63 + created = indexes.DateTimeField(model_attr='created', null=True)
  64 + modified = indexes.DateTimeField(model_attr='modified', null=True)
  65 + type = indexes.CharField()
  66 + icon_name = indexes.CharField()
  67 + tag = indexes.CharField(model_attr='status', null=True)
  68 +
  69 + # trac extra fields
  70 + milestone = indexes.CharField(model_attr='milestone', null=True)
  71 + component = indexes.CharField(model_attr='component', null=True)
  72 + severity = indexes.CharField(model_attr='severity', null=True)
  73 + reporter = indexes.CharField(model_attr='reporter', null=True)
  74 + keywords = indexes.CharField(model_attr='keywords', null=True)
  75 + collaborators = indexes.CharField(model_attr='collaborators', null=True)
  76 +
  77 + def get_model(self):
  78 + return Ticket
  79 +
  80 + def get_updated_field(self):
  81 + return 'modified'
  82 +
  83 + def prepare_author(self, obj):
  84 + author = obj.get_author()
  85 + if author:
  86 + return author.get_full_name()
  87 + return obj.author
  88 +
  89 + def prepare_author_url(self, obj):
  90 + author = obj.get_author()
  91 + if author:
  92 + return author.get_absolute_url()
  93 + return None
  94 +
  95 + def prepare_description(self, obj):
  96 + return u'{}\n{}\n{}\n{}\n{}\n{}\n{}'.format(
  97 + obj.description, obj.milestone, obj.component, obj.severity,
  98 + obj.reporter, obj.keywords, obj.collaborators
  99 + )
  100 +
  101 + def prepare_icon_name(self, obj):
  102 + return u'tag'
  103 +
  104 + def prepare_title(self, obj):
  105 + return u'#{} - {}'.format(obj.pk, obj.summary)
  106 +
  107 + def prepare_type(self, obj):
  108 + return 'ticket'
  109 +
  110 +
  111 +class RevisionIndex(indexes.SearchIndex, indexes.Indexable):
  112 + # common fields
  113 + text = indexes.CharField(document=True, use_template=True)
  114 + url = indexes.CharField(model_attr='get_absolute_url')
  115 + title = indexes.CharField()
  116 + description = indexes.CharField(model_attr='message', null=True)
  117 + author = indexes.CharField(null=True)
  118 + author_url = indexes.CharField(null=True)
  119 + created = indexes.DateTimeField(model_attr='created', null=True)
  120 + modified = indexes.DateTimeField(model_attr='created', null=True)
  121 + type = indexes.CharField()
  122 + icon_name = indexes.CharField()
  123 +
  124 + # trac extra fields
  125 + repository_name = indexes.CharField(model_attr='repository_name')
  126 + revision = indexes.CharField(model_attr='rev')
  127 +
  128 + def get_model(self):
  129 + return Revision
  130 +
  131 + def get_updated_field(self):
  132 + return 'created'
  133 +
  134 + def prepare_author(self, obj):
  135 + author = obj.get_author()
  136 + if author:
  137 + return author.get_full_name()
  138 + return obj.author
  139 +
  140 + def prepare_author_url(self, obj):
  141 + author = obj.get_author()
  142 + if author:
  143 + return author.get_absolute_url()
  144 + return None
  145 +
  146 + def prepare_icon_name(self, obj):
  147 + return u'align-right'
  148 +
  149 + def prepare_title(self, obj):
  150 + return u'{} [{}]'.format(obj.repository_name, obj.rev)
  151 +
  152 + def prepare_type(self, obj):
  153 + return 'changeset'
... ...
src/proxy/templates/search/indexes/proxy/revision_text.txt 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +{{ object.repository_name }}
  2 +{{ object.repository_name|slugify }}
  3 +{{ object.rev }}
  4 +{{ object.rev|slugify }}
  5 +{% firstof object.get_author.get_full_name object.author %}
  6 +{% firstof object.get_author.get_full_name|slugify object.author|slugify %}
  7 +{{ object.message }}
  8 +{{ object.message|slugify }}
... ...
src/proxy/templates/search/indexes/proxy/ticket_text.txt 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +{{ object.summary }}
  2 +{{ object.summary|slugify }}
  3 +{{ object.description }}
  4 +{{ object.description|slugify }}
  5 +{{ object.milestone }}
  6 +{{ object.milestone|slugify }}
  7 +{{ object.component|slugify }}
  8 +{{ object.version }}
  9 +{{ object.severity }}
  10 +{{ object.severity|slugify }}
  11 +{{ object.reporter }}
  12 +{{ object.reporter|slugify }}
  13 +{% firstof object.get_author.get_fullname or object.author %}
  14 +{% firstof object.get_author.get_fullname|slugify or object.author|slugify %}
  15 +{{ object.status }}
  16 +{{ object.status|slugify }}
  17 +{{ object.keywords }}
  18 +{{ object.keywords|slugify }}
  19 +{{ object.collaborators }}
  20 +{{ object.collaborators|slugify }}
... ...
src/proxy/templates/search/indexes/proxy/wiki_text.txt 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +{{ object.author }}
  2 +{{ object.author|slugify }}
  3 +{{ object.name }}
  4 +{{ object.name|slugify }}
  5 +{{ object.collaborators }}
  6 +{{ object.collaborators|slugify }}
  7 +{{ object.wiki_text }}
  8 +{{ object.wiki_text|slugify }}
... ...
src/search/__init__.py 0 → 100644
src/search/admin.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +from django.contrib import admin
  2 +
  3 +# Register your models here.
... ...
src/search/forms.py 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +import unicodedata
  4 +
  5 +from django import forms
  6 +from django.conf import settings
  7 +from django.utils.translation import ugettext_lazy as _
  8 +from haystack.forms import SearchForm
  9 +
  10 +from accounts.models import User
  11 +from super_archives.models import Message
  12 +
  13 +
  14 +class ColabSearchForm(SearchForm):
  15 + q = forms.CharField(label=_('Search'), required=False)
  16 + order = forms.CharField(widget=forms.HiddenInput(), required=False)
  17 + type = forms.CharField(required=False, label=_(u'Type'))
  18 +
  19 + def search(self):
  20 + if not self.is_valid():
  21 + return self.no_query_found()
  22 +
  23 + if self.cleaned_data.get('q'):
  24 + q = unicodedata.normalize(
  25 + 'NFKD', unicode(self.cleaned_data.get('q'))
  26 + ).encode('ascii', 'ignore')
  27 + sqs = self.searchqueryset.auto_query(q)
  28 + else:
  29 + sqs = self.searchqueryset.all()
  30 +
  31 + if self.cleaned_data['type']:
  32 + "It will consider other types with a whitespace"
  33 + types = self.cleaned_data['type']
  34 + sqs = sqs.filter(type__in=types.split())
  35 +
  36 +
  37 + if self.cleaned_data['order']:
  38 + for option, dict_order in settings.ORDERING_DATA.items():
  39 + if self.cleaned_data['order'] == option:
  40 + if dict_order['fields']:
  41 + sqs = sqs.order_by(*dict_order['fields'])
  42 + # if self.cleaned_data['type'] == 'user':
  43 + # sqs = self.searchqueryset.models(User)
  44 + # elif self.cleaned_data['type'] in ['message', 'thread']:
  45 + # sqs = self.searchqueryset.models(Message)
  46 + # elif self.cleaned_data['type'] == 'wiki':
  47 + # sqs = self.searchqueryset.models(Wiki)
  48 + # elif self.cleaned_data['type'] == 'changeset':
  49 + # sqs = self.searchqueryset.models(Changeset)
  50 + # elif self.cleaned_data['type'] == 'ticket':
  51 + # sqs = self.searchqueryset.models(Ticket)
  52 + # else:
  53 + # sqs = self.searchqueryset.all()
  54 +
  55 +
  56 + if self.load_all:
  57 + sqs = sqs.load_all()
  58 +
  59 + return sqs
... ...
src/search/models.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +from django.db import models
  2 +
  3 +# Create your models here.
... ...
src/search/tests.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +from django.test import TestCase
  2 +
  3 +# Create your tests here.
... ...
src/search/urls.py 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +from django.conf.urls import patterns, include, url
  2 +from haystack.query import SearchQuerySet
  3 +
  4 +from .forms import ColabSearchForm
  5 +from .views import ColabSearchView
  6 +
  7 +
  8 +urlpatterns = patterns('',
  9 + url(r'^$', ColabSearchView(
  10 + template='search/search.html',
  11 + searchqueryset=SearchQuerySet(),
  12 + form_class=ColabSearchForm,
  13 + ), name='haystack_search'),
  14 +)
... ...
src/search/views.py 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +# -*- coding:utf-8 -*-
  2 +
  3 +from django.conf import settings
  4 +from haystack.views import SearchView
  5 +
  6 +
  7 +class ColabSearchView(SearchView):
  8 + def extra_context(self, *args, **kwargs):
  9 + # Retornar todos os campos de cada tipo a serem filtrados
  10 + # retornar os nomes dos campos
  11 + # retornar os ícones dos tipos
  12 +
  13 + # a critical point on the system
  14 + types = {
  15 + 'wiki': {
  16 + 'icon': 'file',
  17 + 'fields': [
  18 + 'title', 'description', 'author', 'collaborators',
  19 + 'created', 'modified',
  20 + ],
  21 + },
  22 + 'discussion': {
  23 + 'icon': 'thread',
  24 + 'fields': [
  25 + 'title', 'description', 'created', 'modified', 'author',
  26 + 'tag',
  27 + ],
  28 + },
  29 + 'ticket': {
  30 + 'icon': 'ticket',
  31 + 'fields': [
  32 + 'title', 'description', 'milestone', 'priority',
  33 + 'component', 'version', 'severity', 'reporter', 'author',
  34 + 'status', 'keywords', 'collaborators', 'created',
  35 + 'modified',
  36 + ],
  37 + },
  38 + 'changeset': {
  39 + 'icon': 'changeset',
  40 + 'fields': [
  41 + 'title', 'author', 'description', 'repository_name',
  42 + 'created', 'modified',
  43 + ],
  44 + },
  45 + 'user': {
  46 + 'icon': 'user',
  47 + 'fields': [
  48 + 'title', 'description', 'username', 'name',
  49 + 'email', 'institution', 'role', 'google_talk', 'webpage',
  50 + ],
  51 + },
  52 + }
  53 + types = self.form.cleaned_data['type']
  54 + return dict(
  55 + types=types.split(),
  56 + types_str=types,
  57 + order_data=settings.ORDERING_DATA
  58 + )
... ...
src/static/css/screen.css
... ... @@ -374,3 +374,8 @@ ul.emails {
374 374 }
375 375  
376 376 /* end user profile update */
  377 +
  378 +/* search highlighting */
  379 +span.highlighted {
  380 + background-color: yellow;
  381 +}
... ...
src/super_archives/management/commands/import_emails.py
... ... @@ -21,35 +21,35 @@ class Command(BaseCommand, object):
21 21 """Get emails from mailman archives and import them in the django db. """
22 22  
23 23 help = __doc__
24   -
  24 +
25 25 default_archives_path = '/var/lib/mailman/archives/private'
26 26 RE_SUBJECT_CLEAN = re.compile('((re|res|fw|fwd|en|enc):)|\[.*?\]',
27 27 re.IGNORECASE)
28 28 THREAD_CACHE = {}
29 29 EMAIL_ADDR_CACHE = {}
30   -
  30 +
31 31 # A new command line option to get the dump file to parse.
32 32 option_list = BaseCommand.option_list + (
33 33 make_option('--archives_path',
34 34 dest='archives_path',
35   - help='Path of email archives to be imported. (default: %s)' %
  35 + help='Path of email archives to be imported. (default: %s)' %
36 36 default_archives_path,
37 37 default=default_archives_path),
38   -
  38 +
39 39 make_option('--exclude-list',
40 40 dest='exclude_lists',
41   - help=("Mailing list that won't be imported. It can be used many"
  41 + help=("Mailing list that won't be imported. It can be used many"
42 42 "times for more than one list."),
43 43 action='append',
44 44 default=None),
45   -
  45 +
46 46 make_option('--all',
47 47 dest='all',
48 48 help='Import all messages (default: False)',
49 49 action="store_true",
50 50 default=False),
51 51 )
52   -
  52 +
53 53 def __init__(self, *args, **kwargs):
54 54 super(Command, self).__init__(*args, **kwargs)
55 55  
... ... @@ -68,18 +68,18 @@ class Command(BaseCommand, object):
68 68  
69 69 Yield: An instance of `mailbox.mboxMessage` for each email in the
70 70 file.
71   -
  71 +
72 72 """
73 73 self.log("Parsing email dump: %s." % email_filename)
74 74 mbox = mailbox.mbox(email_filename, factory=CustomMessage)
75   -
  75 +
76 76 # Get each email from mbox file
77 77 #
78 78 # The following implementation was used because the object
79   - # mbox does not support slicing. Converting the object to a
80   - # tuple (as represented in the code down here) was a valid
  79 + # mbox does not support slicing. Converting the object to a
  80 + # tuple (as represented in the code down here) was a valid
81 81 # option but its performance was too poor.
82   - #
  82 + #
83 83 #for message in tuple(mbox)[index:]:
84 84 # yield message
85 85 #
... ... @@ -90,8 +90,8 @@ class Command(BaseCommand, object):
90 90  
91 91 def get_emails(self, mailinglist_dir, all, exclude_lists):
92 92 """Generator function that get the emails from each mailing
93   - list dump dirctory. If `all` is set to True all the emails in the
94   - mbox will be imported if not it will just resume from the last
  93 + list dump dirctory. If `all` is set to True all the emails in the
  94 + mbox will be imported if not it will just resume from the last
95 95 message previously imported. The lists set in `exclude_lists`
96 96 won't be imported.
97 97  
... ... @@ -99,20 +99,20 @@ class Command(BaseCommand, object):
99 99  
100 100 """
101 101 self.log("Getting emails dumps from: %s" % mailinglist_dir)
102   -
  102 +
103 103 # Get the list of directories ending with .mbox
104   - mailing_lists_mboxes = (mbox for mbox in os.listdir(mailinglist_dir)
  104 + mailing_lists_mboxes = (mbox for mbox in os.listdir(mailinglist_dir)
105 105 if mbox.endswith('.mbox'))
106   -
  106 +
107 107 # Get messages from each mbox
108 108 for mbox in mailing_lists_mboxes:
109 109 mbox_path = os.path.join(mailinglist_dir, mbox, mbox)
110 110 mailinglist_name = mbox.split('.')[0]
111   -
  111 +
112 112 # Check if the mailinglist is set not to be imported
113 113 if exclude_lists and mailinglist_name in exclude_lists:
114 114 continue
115   -
  115 +
116 116 # Find the index of the last imported message
117 117 if all:
118 118 n_msgs = 0
... ... @@ -123,13 +123,13 @@ class Command(BaseCommand, object):
123 123 n_msgs = mailinglist.last_imported_index
124 124 except MailingList.DoesNotExist:
125 125 n_msgs = 0
126   -
  126 +
127 127 for index, msg in self.parse_emails(mbox_path, n_msgs):
128 128 yield mailinglist_name, msg, index
129 129  
130 130 def get_thread(self, email, mailinglist):
131 131 """Group messages by thread looking for similar subjects"""
132   -
  132 +
133 133 subject_slug = slugify(email.subject_clean)
134 134 thread = self.THREAD_CACHE.get(subject_slug, {}).get(mailinglist.id)
135 135 if thread is None:
... ... @@ -137,27 +137,27 @@ class Command(BaseCommand, object):
137 137 mailinglist=mailinglist,
138 138 subject_token=subject_slug
139 139 )[0]
140   -
  140 +
141 141 if self.THREAD_CACHE.get(subject_slug) is None:
142 142 self.THREAD_CACHE[subject_slug] = dict()
143 143 self.THREAD_CACHE[subject_slug][mailinglist.id] = thread
144 144  
145 145 thread.latest_message = email
146   - thread.save()
  146 + thread.save()
147 147 return thread
148   -
  148 +
149 149 def save_email(self, list_name, email_msg, index):
150 150 """Save email message into the database."""
151   -
  151 +
152 152 # Update last imported message into the DB
153 153 mailinglist, created = MailingList.objects.get_or_create(name=list_name)
154 154 mailinglist.last_imported_index = index
155   -
156   - if created:
  155 +
  156 + if created:
157 157 # if the mailinglist is newly created it's sure that the message
158 158 # is not in the DB yet.
159 159 self.create_email(mailinglist, email_msg)
160   -
  160 +
161 161 else:
162 162 # If the message is already at the database don't do anything
163 163 try:
... ... @@ -165,11 +165,11 @@ class Command(BaseCommand, object):
165 165 message_id=email_msg.get('Message-ID'),
166 166 thread__mailinglist=mailinglist
167 167 )
168   -
  168 +
169 169 except Message.DoesNotExist:
170 170 self.create_email(mailinglist, email_msg)
171   -
172   - mailinglist.save()
  171 +
  172 + mailinglist.save()
173 173  
174 174 def create_email(self, mailinglist, email_msg):
175 175  
... ... @@ -198,59 +198,59 @@ class Command(BaseCommand, object):
198 198 email.thread = self.get_thread(email, mailinglist)
199 199 email.save()
200 200  
201   - @transaction.commit_manually
  201 + @transaction.commit_manually
202 202 def import_emails(self, archives_path, all, exclude_lists=None):
203   - """Get emails from the filesystem from the `archives_path`
204   - and store them into the database. If `all` is set to True all
205   - the filesystem storage will be imported otherwise the
206   - importation will resume from the last message previously
  203 + """Get emails from the filesystem from the `archives_path`
  204 + and store them into the database. If `all` is set to True all
  205 + the filesystem storage will be imported otherwise the
  206 + importation will resume from the last message previously
207 207 imported. The lists set in `exclude_lists` won't be imported.
208   -
  208 +
209 209 """
210   -
  210 +
211 211 count = 0
212 212 email_generator = self.get_emails(archives_path, all, exclude_lists)
213 213 for mailinglist_name, msg, index in email_generator:
214 214 try:
215 215 self.save_email(mailinglist_name, msg, index)
216 216 except:
217   - # This anti-pattern is needed to avoid the transations to
  217 + # This anti-pattern is needed to avoid the transations to
218 218 # get stuck in case of errors.
219 219 transaction.rollback()
220 220 raise
221   -
  221 +
222 222 count += 1
223 223 if count % 1000 == 0:
224 224 transaction.commit()
225   -
  225 +
226 226 transaction.commit()
227   -
  227 +
228 228 def handle(self, *args, **options):
229 229 """Main command method."""
230   -
  230 +
231 231 lock_file = '/var/lock/colab/import_emails.lock'
232   -
  232 +
233 233 # Already running, so quit
234 234 if os.path.exists(lock_file):
235 235 self.log(("This script is already running. (If your are sure it's "
236 236 "not please delete the lock file in %s')") % lock_file)
237 237 sys.exit(0)
238   -
  238 +
239 239 if not os.path.exists(os.path.dirname(lock_file)):
240 240 os.mkdir(os.path.dirname(lock_file), 0755)
241   -
  241 +
242 242 run_lock = file(lock_file, 'w')
243 243 run_lock.close()
244   -
  244 +
245 245 archives_path = options.get('archives_path')
246 246 self.log('Using archives_path `%s`' % self.default_archives_path)
247   -
  247 +
248 248 if not os.path.exists(archives_path):
249 249 raise CommandError('archives_path (%s) does not exist' %
250 250 archives_path)
251   -
252   - self.import_emails(archives_path,
  251 +
  252 + self.import_emails(archives_path,
253 253 options.get('all'), options.get('exclude_lists'))
254   -
  254 +
255 255 os.remove(lock_file)
256   -
  256 +
... ...
src/super_archives/models.py
... ... @@ -79,6 +79,9 @@ class MailingList(models.Model):
79 79 logo = models.FileField(upload_to='list_logo') #TODO
80 80 last_imported_index = models.IntegerField(default=0)
81 81  
  82 + def get_absolute_url(self):
  83 + return u'{}?list={}'.format(reverse('thread_list'), self.name)
  84 +
82 85 def __unicode__(self):
83 86 return self.name
84 87  
... ... @@ -125,6 +128,10 @@ class Thread(models.Model):
125 128 verbose_name_plural = _(u"Threads")
126 129 unique_together = ('subject_token', 'mailinglist')
127 130  
  131 + @models.permalink
  132 + def get_absolute_url(self):
  133 + return ('thread_view', [self.mailinglist, self.subject_token])
  134 +
128 135 def update_keywords(self):
129 136 blocks = MessageBlock.objects.filter(message__thread__pk=self.pk,
130 137 is_reply=False)
... ...
src/super_archives/search_indexes.py 0 → 100644
... ... @@ -0,0 +1,55 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +from haystack import indexes
  4 +
  5 +from .models import Thread
  6 +
  7 +
  8 +class ThreadIndex(indexes.SearchIndex, indexes.Indexable):
  9 + # common fields
  10 + text = indexes.CharField(document=True, use_template=True)
  11 + url = indexes.CharField(model_attr='get_absolute_url', null=True)
  12 + title = indexes.CharField(model_attr='latest_message__subject_clean')
  13 + description = indexes.CharField(use_template=True)
  14 + created = indexes.DateTimeField()
  15 + modified = indexes.DateTimeField(
  16 + model_attr='latest_message__received_time'
  17 + )
  18 + author = indexes.CharField(null=True)
  19 + author_url = indexes.CharField(null=True)
  20 + type = indexes.CharField()
  21 + icon_name = indexes.CharField()
  22 + tag = indexes.CharField(model_attr='mailinglist__name')
  23 +
  24 + mailinglist_url = indexes.CharField(
  25 + model_attr='mailinglist__get_absolute_url'
  26 + )
  27 +
  28 + def get_model(self):
  29 + return Thread
  30 +
  31 + def get_updated_field(self):
  32 + return 'received_time'
  33 +
  34 + def prepare_author(self, obj):
  35 + return obj.message_set.first().from_address.get_full_name()
  36 +
  37 + def prepare_author_url(self, obj):
  38 + first_message = obj.message_set.first()
  39 + if first_message.from_address.user:
  40 + return first_message.from_address.user.get_absolute_url()
  41 + return None
  42 +
  43 + def prepare_created(self, obj):
  44 + return obj.message_set.first().received_time
  45 +
  46 + def prepare_icon_name(self, obj):
  47 + return u'envelope'
  48 +
  49 + def prepare_type(self, obj):
  50 + return u'thread'
  51 +
  52 + def index_queryset(self, using=None):
  53 + return self.get_model().objects.filter(
  54 + spam=False
  55 + ).exclude(subject_token='')
... ...
src/super_archives/templates/message-list.html
... ... @@ -12,18 +12,24 @@
12 12  
13 13 <h4>{% trans "Sort by" %}</h4>
14 14 <ul class="unstyled-list">
15   - <li {% ifequal order_by "hottest" %} title="{% trans "Remove filter" %}" {% endifequal %}>
16   - <span class="glyphicon glyphicon-chevron-right"></span> <a href="{% ifequal order_by "hottest" %} {% append_to_get order="",p=1 %} {% else %} {% append_to_get order='hottest',p=1 %} {% endifequal %}">{% trans "Relevance" %}</a></li>
17   - <li {% ifequal order_by "latest" %} title="{% trans "Remove filter" %}" {% endifequal %}>
18   - <span class="glyphicon glyphicon-chevron-right"></span> <a href="{% ifequal order_by "latest" %} {% append_to_get order="",p=1 %} {% else %} {% append_to_get order='latest',p=1 %} {% endifequal %}">
19   - {% trans "Recent activity" %}</a></li>
  15 + {% for option, dict_order in order_data.items %}
  16 + <li>
  17 + <span class="glyphicon glyphicon-chevron-right"></span>
  18 + <a href="{% append_to_get order=option p=1 %}">
  19 + {% ifequal request.GET.order option %}
  20 + {% blocktrans with name=dict_order.name %}<strong>{{ name }}</strong>{% endblocktrans %}</a>
  21 + {% else %}
  22 + {% blocktrans with name=dict_order.name %}{{ name }}{% endblocktrans %}</a>
  23 + {% endifequal %}
  24 + </li>
  25 + {% endfor %}
20 26 </ul>
21 27  
22 28 <h4>{% trans "Lists" %}</h4>
23 29 <ul class="unstyled-list">
24 30 {% for list in lists %}
25 31 <li {% if list.name == selected_list %} title="{% trans "Remove filter" %}" class="selected" {% endif %}>
26   - <span class="glyphicon {% if list.name == selected_list %}glyphicon-remove{% else %}glyphicon-chevron-right{% endif %}"></span> <a href="{% ifnotequal list.name selected_list %} {% append_to_get list=list.name,p=1 %} {% else %} {% append_to_get list="",p=1 %}
  32 + <span class="glyphicon {% if list.name == selected_list %}glyphicon-remove{% else %}glyphicon-chevron-right{% endif %}"></span> <a href="{% ifnotequal list.name selected_list %} {% append_to_get list=list.name p=1 %} {% else %} {% append_to_get list="" p=1 %}
27 33 {% endifnotequal %}">{{ list.name }}</a></li>
28 34 {% endfor %}
29 35 </ul>
... ...
src/super_archives/templates/search/indexes/super_archives/thread_description.txt 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +{% for message in object.message_set.iterator %}
  2 + {% if not spam %}
  3 + {{ message.subject_clean }}
  4 + {{ message.subject_clean|slugify }}
  5 + {{ message.body }}
  6 + {{ message.body|slugify }}
  7 + {{ message.from_address.get_full_name }}
  8 + {{ message.from_address.get_full_name|slugify }}
  9 + {% endif %}
  10 +{% endfor %}
... ...
src/super_archives/templates/search/indexes/super_archives/thread_text.txt 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +{{ object.thread.mailinglist.name }}
  2 +{{ object.thread.mailinglist.name|slugify }}
  3 +{{ object.thread.subject_token }}
  4 +
  5 +{{ object.body }}
  6 +{{ object.body|slugify }}
  7 +{{ object.subject_clean }}
  8 +{{ object.subject_clean|slugify }}
  9 +
  10 +{{ object.from_address.get_full_name }}
  11 +
  12 +{% for message in object.message_set.iterator %}
  13 + {% if not spam %}
  14 + {{ message.subject_clean }}
  15 + {{ message.subject_clean|slugify }}
  16 + {{ message.body }}
  17 + {{ message.body|slugify }}
  18 + {{ message.from_address.get_full_name }}
  19 + {{ message.from_address.get_full_name|slugify }}
  20 + {% endif %}
  21 +{% endfor %}
... ...
src/super_archives/templatetags/append_to_get.py
... ... @@ -1,56 +0,0 @@
1   -
2   -import urllib
3   -from django import template
4   -
5   -register = template.Library()
6   -
7   -"""
8   -Decorator to facilitate template tag creation
9   -"""
10   -def easy_tag(func):
11   - """deal with the repetitive parts of parsing template tags"""
12   - def inner(parser, token):
13   - #print token
14   - try:
15   - return func(*token.split_contents())
16   - except TypeError:
17   - raise template.TemplateSyntaxError('Bad arguments for tag "%s"' %
18   - token.split_contents()[0])
19   - inner.__name__ = func.__name__
20   - inner.__doc__ = inner.__doc__
21   - return inner
22   -
23   -
24   -class AppendGetNode(template.Node):
25   - def __init__(self, dict):
26   - self.dict_pairs = {}
27   - for pair in dict.split(','):
28   - pair = pair.split('=')
29   - self.dict_pairs[pair[0]] = template.Variable(pair[1])
30   -
31   - def render(self, context):
32   - get = context['request'].GET.copy()
33   -
34   - for key in self.dict_pairs:
35   - get[key] = self.dict_pairs[key].resolve(context)
36   -
37   - path = context['request'].META['PATH_INFO']
38   -
39   - if len(get):
40   - # Convert all unicode objects in the get dict to
41   - # str (utf-8 encoded)
42   - get_utf_encoded = {}
43   - for (key, value) in get.items():
44   - if isinstance(value, unicode):
45   - value = value.encode('utf-8')
46   - get_utf_encoded.update({key: value})
47   - get_utf_encoded = dict(get_utf_encoded)
48   -
49   - path = '?' + urllib.urlencode(get_utf_encoded)
50   -
51   - return path
52   -
53   -@register.tag()
54   -@easy_tag
55   -def append_to_get(_tag_name, dict):
56   - return AppendGetNode(dict)
src/super_archives/templatetags/urlutils.py 0 → 100644
... ... @@ -0,0 +1,24 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +from django import template
  4 +
  5 +from super_archives.utils import url
  6 +
  7 +register = template.Library()
  8 +
  9 +
  10 +@register.simple_tag(takes_context=True)
  11 +def append_to_get(context, **kwargs):
  12 + return url.append_to_get(
  13 + context['request'].META['PATH_INFO'],
  14 + context['request'].META['QUERY_STRING'],
  15 + **kwargs
  16 + )
  17 +
  18 +@register.simple_tag(takes_context=True)
  19 +def pop_from_get(context, **kwargs):
  20 + return url.pop_from_get(
  21 + context['request'].META['PATH_INFO'],
  22 + context['request'].META['QUERY_STRING'],
  23 + **kwargs
  24 + )
... ...
src/super_archives/utils/url.py 0 → 100644
... ... @@ -0,0 +1,83 @@
  1 +# -*- coding: utf-8 -*-
  2 +
  3 +def append_to_get(path, query=None, **kwargs):
  4 +# Getting the path with the query
  5 + current_url = u'{}?{}'.format(
  6 + path,
  7 + query,
  8 + )
  9 +
  10 + if kwargs and query:
  11 + current_url += '&'
  12 +
  13 + for key, value in kwargs.items():
  14 + # get the key, value to check if the pair exists in the query
  15 + new = u'{}={}'.format(key, value)
  16 +
  17 + if new in current_url:
  18 + continue
  19 +
  20 + if key not in current_url:
  21 + current_url += u'{}={}&'.format(key, value)
  22 + continue
  23 +
  24 + parse_url = current_url.split(key)
  25 +
  26 + if len(parse_url) > 2:
  27 + continue
  28 +
  29 + if unicode(value) in parse_url[1][1:]:
  30 + continue
  31 +
  32 + check_kwargs_values = [
  33 + False for value in kwargs.values()
  34 + if unicode(value) not in parse_url[1]
  35 + ]
  36 +
  37 + if not all(check_kwargs_values):
  38 + list_remaining = parse_url[1][1:].split('&')
  39 + real_remaining = u''
  40 +
  41 + if len(list_remaining) >= 2:
  42 + real_remaining = u'&'.join(list_remaining[1:])
  43 +
  44 + current_url = u'{url}{key}={value}&{remaining}'.format(
  45 + url=parse_url[0],
  46 + key=key,
  47 + value=value,
  48 + remaining=real_remaining,
  49 + )
  50 + continue
  51 +
  52 + current_url = u'{url}{key}={value}+{remaining_get}'.format(
  53 + url=parse_url[0],
  54 + key=key,
  55 + value=value,
  56 + remaining_get=parse_url[1][1:],
  57 + )
  58 + if current_url[-1] == '&':
  59 + return current_url[:-1]
  60 + return current_url
  61 +
  62 +
  63 +def pop_from_get(path, query=None, **kwargs):
  64 + # Getting the path with the query
  65 + print query
  66 +
  67 + current_url = u'{}?{}'.format(
  68 + path,
  69 + query,
  70 + )
  71 + for key, value in kwargs.items():
  72 + popitem = u'{}={}'.format(key, value)
  73 + if query == popitem:
  74 + return path
  75 +
  76 + if key not in current_url:
  77 + return current_url
  78 +
  79 + first_path, end_path = current_url.split(key)
  80 + end_path_without_element = end_path.split(value, 1)
  81 + path_list = first_path + end_path_without_element
  82 + print path_list
  83 + return u''.join(path_list)
... ...
src/super_archives/views.py
... ... @@ -3,6 +3,7 @@
3 3 import smtplib
4 4  
5 5 from django import http
  6 +from django.conf import settings
6 7 from django.contrib import messages
7 8 from django.db import IntegrityError
8 9 from django.views.generic import View
... ... @@ -85,7 +86,7 @@ def list_messages(request):
85 86 'n_results': paginator.count,
86 87 'threads': threads,
87 88 'selected_list': selected_list,
88   - 'order_by': order_by,
  89 + 'order_data': settings.ORDERING_DATA,
89 90 }
90 91 return render(request, 'message-list.html', template_data)
91 92  
... ...
src/templates/base.html
... ... @@ -111,7 +111,7 @@
111 111 {% endif %}
112 112 </ul>
113 113  
114   - <form action="/search/" method="GET" id="search-form" class="navbar-form navbar-right hidden-xs hidden-sm" role="search">
  114 + <form action="{% url 'haystack_search' %}" method="GET" id="search-form" class="navbar-form navbar-right hidden-xs hidden-sm" role="search">
115 115 <div class="form-group">
116 116 <label class="sr-only" for="header-searchbox">{% trans 'Search here...' %}</label>
117 117 <input name="q" id="header-searchbox"
... ...
src/templates/home.html
... ... @@ -27,12 +27,12 @@
27 27 title="{% trans 'RSS - Latest collaborations' %}">
28 28 </a>
29 29 <ul class="message-list">
30   - {% for doc in latest_docs %}
31   - {% include "message-preview.html" %}
  30 + {% for result in latest_results %}
  31 + {% include "search/preview-search.html" %}
32 32 {% endfor %}
33 33 </ul>
34 34 <a class="column-align"
35   - href="{% url 'search' %}?o=modified+desc">
  35 + href="{% url 'haystack_search' %}?order=latest">
36 36 {% trans "View more collaborations..." %}
37 37 </a>
38 38 <div>&nbsp;</div>
... ...
src/templates/search/preview-search.html 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +{% load i18n %}
  2 +{% load highlight %}
  3 +
  4 +<li class="preview-message">
  5 +<span class="glyphicon glyphicon-{{ result.icon_name }}" title="{{ result.type }}"></span>
  6 +
  7 +{% if result.tag %}
  8 +<a href="{% firstof result.mailinglist_url result.url %}">
  9 + <span class="label label-primary">{{ result.tag }}</span>
  10 +</a>
  11 +{% endif %}
  12 +
  13 +{% if result.title %}
  14 + <a href="{{ result.url }}" {% if result.description %}title="{{ result.description|escape|truncatechars:200 }}"{% endif %}>
  15 + <span class="subject">
  16 + <!-- a striptags filter was raising an error here because using with highlight -->
  17 + {% if query %}
  18 + {% highlight result.title with query max_length "1000" %}
  19 + {% else %}
  20 + {{ result.title }}
  21 + {% endif %}
  22 + </span>
  23 + </a>
  24 +{% endif %}
  25 +
  26 +{% if result.description %}
  27 + <!-- a striptags filter was raising an error here because using with highlight -->
  28 + <span class="quiet">- {% if query %}{% highlight result.description with query max_length "150" %}{% else %}{{ result.description }}{% endif %}</span>
  29 +{% endif %}
  30 +
  31 +{% if result.author or result.modified %}
  32 + <div class="quiet">
  33 + {% if result.author %}
  34 + <span class="pull-left">{% trans "by" %}
  35 + {% if result.author and result.author_url %}
  36 + <a href="{{ result.author_url }}">
  37 + {% if query %}
  38 + {% highlight result.author with query %}
  39 + {% else %}
  40 + {{ result.author }}
  41 + {% endif %}
  42 + </a>
  43 + {% else %}
  44 + <span>{{ result.author }}</span>
  45 + {% endif %}
  46 + </span>
  47 + {% endif %}
  48 + {% if result.modified %}
  49 + <span class="pull-right">{{ result.modified|timesince }} {% trans "ago" %}</span>
  50 + {% endif %}
  51 + </div>
  52 +{% endif %}
  53 +</li>
... ...
src/templates/search/search-message-preview.html 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +{% load i18n %}
  2 +
  3 +<span class="glyphicon glyphicon-envelope" title="{{ result.type }}"></span>
  4 +
  5 +{% if result.mailinglist %}
  6 + <a href="{% url 'super_archives.views.list_messages' %}?list={{ result.mailinglist }}">
  7 + <span class="label label-primary">{{ result.mailinglist }}</span>
  8 + </a>
  9 +{% endif %}
  10 +
  11 +<span class="subject">
  12 + <a href="{{ result.url }}#msg-{{ result.pk }}"
  13 + title="{% filter striptags|truncatewords:50 %}{{ result.description|escape }}{% endfilter %}">
  14 + {{ result.title }}
  15 + </a>
  16 +</span>
  17 +
  18 +<span class="quiet">- {{ result.description|striptags }}</span>
... ...
src/templates/search/search-revision-preview.html 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +{% load i18n %}
  2 +
  3 +<span class="glyphicon glyphicon-align-right" title="{{ result.type }}"></span>
  4 +
  5 +<span class="subject">
  6 + <a href="{{ result.url }}">{{ result.repository_name }} [{{ result.revision }}]</a>
  7 +</span>
  8 +
  9 +<span class="quiet">{% if result.message %}- {{ result.message }}{% endif %}</span>
... ...
src/templates/search/search-ticket-preview.html 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +{% load i18n %}
  2 +{% load highlight %}
  3 +
  4 +<span class="glyphicon glyphicon-tag" title="{{ result.type }}"></span>
  5 +
  6 +<a href="{{ result.url }}" title="{{ result.description|escape }}">
  7 +{% if result.status %}
  8 + <span class="label label-primary">{{ result.status }}</span>
  9 +{% endif %}
  10 +
  11 +<span class="subject">
  12 + #{{ result.pk }} - {% filter striptags|truncatewords:50 %}{{ result.summary|escape }}{% endfilter %}
  13 + </a>
  14 +</span>
  15 +
  16 +<span class="quiet">- {% highlight result.description with query max_length "150" %}</span>
... ...
src/templates/search/search-user-preview.html 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +{% load i18n %}
  2 +
  3 +<span class="glyphicon glyphicon-user" title="{{ result.type }}"></span>
  4 +
  5 +<span class="subject">
  6 + <a href="{% url 'user_profile' result.username %}">{{ result.name }}</a>
  7 +</span>
  8 +
  9 +<span class="quiet">{% if result.institution %}- {{ result.institution }}{% endif %}{% if result.role %} - {{ result.role }}{% endif %}</span>
... ...
src/templates/search/search-wiki-preview.html 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +{% load i18n %}
  2 +
  3 +<span class="glyphicon glyphicon-file" title="{{ result.type }}"></span>
  4 +
  5 +<span class="subject">
  6 + <a href="{{ result.url }}">{{ result.name }}</a>
  7 +</span>
  8 +
  9 +<span class="quiet">{% if result.wiki_text %}- {{ result.wiki_text|truncatechars:150 }}{% elif result.comment %}- {{ result.comment|truncatechars:150 }}{% endif %}</span>
... ...
src/templates/search/search.html 0 → 100644
... ... @@ -0,0 +1,96 @@
  1 +{% extends "base.html" %}
  2 +{% load i18n %}
  3 +{% load urlutils %}
  4 +{% load highlight %}
  5 +
  6 +{% block main-content %}
  7 + <div class="row">
  8 + <div class="col-lg-2">
  9 + <h2>{% trans "Search" %}</h2>
  10 + </div>
  11 + <span class="pull-right quiet">
  12 + {{ page.paginator.count }} {% trans "documents found" %}
  13 + </span>
  14 + </div>
  15 +
  16 + <hr/>
  17 +
  18 + <div class="row">
  19 + <div id="filters" class="hidden-xs hidden-sm col-md-2 col-lg-2">
  20 + <h3>{% trans "Filters" %}</h3>
  21 +
  22 + <h4>{% trans "Sort by" %}</h4>
  23 + <ul class="unstyled-list">
  24 + {% for option, dict_order in order_data.items %}
  25 + <li>
  26 + <span class="glyphicon glyphicon-chevron-right"></span>
  27 + <a href="{% append_to_get order=option p=1 %}">
  28 + {% ifequal request.GET.order option %}
  29 + {% blocktrans with name=dict_order.name %}<strong>{{ name }}</strong>{% endblocktrans %}
  30 + {% else %}
  31 + {% blocktrans with name=dict_order.name %}{{ name }}{% endblocktrans %}
  32 + {% endifequal %}
  33 + </a>
  34 + </li>
  35 + {% endfor %}
  36 + </ul>
  37 +
  38 + <h4>{% trans "Types" %}</h4>
  39 +
  40 + <ul class="unstyled-list">
  41 + <li>
  42 + <span class="glyphicon glyphicon-file"></span>
  43 + <a href="{% append_to_get type='wiki' %}">{% trans "Wiki" %}</a>
  44 + </li>
  45 + <li>
  46 + <span class="glyphicon glyphicon-envelope"></span>
  47 + <a href="{% append_to_get type='thread' %}">{% trans "Discussion" %}</a>
  48 + </li>
  49 + <li>
  50 + <span class="glyphicon glyphicon-tag"></span>
  51 + <a href="{% append_to_get type='ticket' %}">{% trans "Ticket" %}</a>
  52 + </li>
  53 + <li>
  54 + <span class="glyphicon glyphicon-align-right"></span>
  55 + <a href="{% append_to_get type='changeset' %}">{% trans "Changeset" %}</a>
  56 + </li>
  57 + <li>
  58 + <span class="glyphicon glyphicon-user"></span>
  59 + <a href="{% append_to_get type='user' %}">{% trans "User" %}</a>
  60 + </li>
  61 + </ul>
  62 + </div>
  63 +
  64 + <div class="col-lg-10">
  65 + <ul class="list-unstyled">
  66 + {% for result in page.object_list %}
  67 + {% include "search/preview-search.html" %}
  68 + {% empty %}
  69 + <li class="text-center">
  70 + {% trans "No results for your search." %}
  71 + </li>
  72 + {% endfor %}
  73 + </ul>
  74 +
  75 + {% if query and page.has_other_pages %}
  76 + <div class="text-center">
  77 + <span>
  78 + {% if page.has_previous %}
  79 + <a href="{% append_to_get page=page.previous_page_number %}">{% trans "Previous" %}</a>
  80 + {% endif %}
  81 + <span>
  82 + {% trans "Page" %} {{ page.number }} {% trans "of" %}
  83 + {{ page.paginator.num_pages }}
  84 + </span>
  85 +
  86 + {% if page.has_next %}
  87 + <a href="{% append_to_get page=page.next_page_number %}">{% trans "Next" %}</a>
  88 + {% endif %}
  89 + </span>
  90 + </div>
  91 + {% endif %}
  92 +
  93 + </div>
  94 + </div>
  95 +
  96 +{% endblock %}
... ...