Commit 8d6f24fc53f695213e2b6b8478d1508fbca16f02
Exists in
master
and in
39 other branches
Merge branch 'master' of github.com:TracyWebTech/colab
Showing
43 changed files
with
1192 additions
and
128 deletions
Show diff stats
requirements.txt
... | ... | @@ -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
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: | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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) | ... | ... |
... | ... | @@ -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/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 | ... | ... |
... | ... | @@ -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 }} | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | +) | ... | ... |
... | ... | @@ -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
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) | ... | ... |
... | ... | @@ -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) |
... | ... | @@ -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 | + ) | ... | ... |
... | ... | @@ -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> </div> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 %} | ... | ... |