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
@@ -8,6 +8,7 @@ chardet==1.0.1 | @@ -8,6 +8,7 @@ chardet==1.0.1 | ||
8 | python-dateutil==1.5 | 8 | python-dateutil==1.5 |
9 | django-cliauth==0.9 | 9 | django-cliauth==0.9 |
10 | django-mobile==0.3.0 | 10 | django-mobile==0.3.0 |
11 | +django-haystack==2.1 | ||
11 | etiquetando==0.1 | 12 | etiquetando==0.1 |
12 | html2text | 13 | html2text |
13 | django-taggit | 14 | django-taggit |
@@ -0,0 +1,45 @@ | @@ -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,6 +15,56 @@ LANGUAGES = ( | ||
15 | 15 | ||
16 | LANGUAGE_CODE = 'pt-br' | 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 | INSTALLED_APPS = INSTALLED_APPS + ( | 68 | INSTALLED_APPS = INSTALLED_APPS + ( |
19 | 69 | ||
20 | # Not standard apps | 70 | # Not standard apps |
@@ -24,6 +74,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( | @@ -24,6 +74,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( | ||
24 | 'django_mobile', | 74 | 'django_mobile', |
25 | 'django_browserid', | 75 | 'django_browserid', |
26 | 'conversejs', | 76 | 'conversejs', |
77 | + 'haystack', | ||
27 | 78 | ||
28 | # Own apps | 79 | # Own apps |
29 | 'super_archives', | 80 | 'super_archives', |
@@ -33,6 +84,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( | @@ -33,6 +84,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( | ||
33 | 'planet', | 84 | 'planet', |
34 | 'accounts', | 85 | 'accounts', |
35 | 'proxy', | 86 | 'proxy', |
87 | + 'search', | ||
36 | 88 | ||
37 | # Feedzilla and deps | 89 | # Feedzilla and deps |
38 | 'feedzilla', | 90 | 'feedzilla', |
src/colab/deprecated/views/other.py
@@ -6,12 +6,16 @@ other.py | @@ -6,12 +6,16 @@ other.py | ||
6 | Created by Sergio Campos on 2012-01-10. | 6 | Created by Sergio Campos on 2012-01-10. |
7 | """ | 7 | """ |
8 | 8 | ||
9 | +import datetime | ||
10 | + | ||
9 | from django.template import RequestContext | 11 | from django.template import RequestContext |
10 | from django.http import HttpResponseNotAllowed | 12 | from django.http import HttpResponseNotAllowed |
11 | from django.shortcuts import render_to_response | 13 | from django.shortcuts import render_to_response |
14 | +from django.utils import timezone | ||
12 | from django.utils.translation import ugettext as _ | 15 | from django.utils.translation import ugettext as _ |
13 | 16 | ||
14 | -from colab.deprecated import solrutils | 17 | +from haystack.query import SearchQuerySet |
18 | + | ||
15 | from super_archives import queries | 19 | from super_archives import queries |
16 | 20 | ||
17 | 21 | ||
@@ -21,11 +25,21 @@ def home(request): | @@ -21,11 +25,21 @@ def home(request): | ||
21 | latest_threads = queries.get_latest_threads() | 25 | latest_threads = queries.get_latest_threads() |
22 | hottest_threads = queries.get_hottest_threads() | 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 | template_data = { | 36 | template_data = { |
25 | 'hottest_threads': hottest_threads[:6], | 37 | 'hottest_threads': hottest_threads[:6], |
26 | 'latest_threads': latest_threads[:6], | 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 | return render_to_response('home.html', template_data, | 44 | return render_to_response('home.html', template_data, |
31 | context_instance=RequestContext(request)) | 45 | context_instance=RequestContext(request)) |
@@ -34,7 +48,7 @@ def home(request): | @@ -34,7 +48,7 @@ def home(request): | ||
34 | def search(request): | 48 | def search(request): |
35 | if request.method != 'GET': | 49 | if request.method != 'GET': |
36 | return HttpResponseNotAllowed(['GET']) | 50 | return HttpResponseNotAllowed(['GET']) |
37 | - | 51 | + |
38 | query = request.GET.get('q') | 52 | query = request.GET.get('q') |
39 | sort = request.GET.get('o') | 53 | sort = request.GET.get('o') |
40 | type_ = request.GET.get('type') | 54 | type_ = request.GET.get('type') |
@@ -42,7 +56,7 @@ def search(request): | @@ -42,7 +56,7 @@ def search(request): | ||
42 | page_number = int(request.GET.get('p', '1')) | 56 | page_number = int(request.GET.get('p', '1')) |
43 | except ValueError: | 57 | except ValueError: |
44 | page_number = 1 | 58 | page_number = 1 |
45 | - | 59 | + |
46 | try: | 60 | try: |
47 | results_per_page = int(request.GET.get('per_page', 16)) | 61 | results_per_page = int(request.GET.get('per_page', 16)) |
48 | except ValueError: | 62 | except ValueError: |
@@ -0,0 +1,23 @@ | @@ -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 | from django.conf.urls import patterns, include, url | 1 | from django.conf.urls import patterns, include, url |
3 | from django.conf import settings | 2 | from django.conf import settings |
4 | from django.views.generic import TemplateView | 3 | from django.views.generic import TemplateView |
5 | from django.contrib import admin | 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 | admin.autodiscover() | 11 | admin.autodiscover() |
9 | 12 | ||
10 | urlpatterns = patterns('', | 13 | urlpatterns = patterns('', |
11 | url(r'^$', 'colab.deprecated.views.other.home', name='home'), | 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 | url(r'open-data/$', TemplateView.as_view(template_name='open-data.html'), | 17 | url(r'open-data/$', TemplateView.as_view(template_name='open-data.html'), |
16 | name='opendata'), | 18 | name='opendata'), |
17 | 19 |
@@ -0,0 +1,31 @@ | @@ -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 @@ | @@ -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 | from django.db import models | 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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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,35 +21,35 @@ class Command(BaseCommand, object): | ||
21 | """Get emails from mailman archives and import them in the django db. """ | 21 | """Get emails from mailman archives and import them in the django db. """ |
22 | 22 | ||
23 | help = __doc__ | 23 | help = __doc__ |
24 | - | 24 | + |
25 | default_archives_path = '/var/lib/mailman/archives/private' | 25 | default_archives_path = '/var/lib/mailman/archives/private' |
26 | RE_SUBJECT_CLEAN = re.compile('((re|res|fw|fwd|en|enc):)|\[.*?\]', | 26 | RE_SUBJECT_CLEAN = re.compile('((re|res|fw|fwd|en|enc):)|\[.*?\]', |
27 | re.IGNORECASE) | 27 | re.IGNORECASE) |
28 | THREAD_CACHE = {} | 28 | THREAD_CACHE = {} |
29 | EMAIL_ADDR_CACHE = {} | 29 | EMAIL_ADDR_CACHE = {} |
30 | - | 30 | + |
31 | # A new command line option to get the dump file to parse. | 31 | # A new command line option to get the dump file to parse. |
32 | option_list = BaseCommand.option_list + ( | 32 | option_list = BaseCommand.option_list + ( |
33 | make_option('--archives_path', | 33 | make_option('--archives_path', |
34 | dest='archives_path', | 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 | default_archives_path, | 36 | default_archives_path, |
37 | default=default_archives_path), | 37 | default=default_archives_path), |
38 | - | 38 | + |
39 | make_option('--exclude-list', | 39 | make_option('--exclude-list', |
40 | dest='exclude_lists', | 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 | "times for more than one list."), | 42 | "times for more than one list."), |
43 | action='append', | 43 | action='append', |
44 | default=None), | 44 | default=None), |
45 | - | 45 | + |
46 | make_option('--all', | 46 | make_option('--all', |
47 | dest='all', | 47 | dest='all', |
48 | help='Import all messages (default: False)', | 48 | help='Import all messages (default: False)', |
49 | action="store_true", | 49 | action="store_true", |
50 | default=False), | 50 | default=False), |
51 | ) | 51 | ) |
52 | - | 52 | + |
53 | def __init__(self, *args, **kwargs): | 53 | def __init__(self, *args, **kwargs): |
54 | super(Command, self).__init__(*args, **kwargs) | 54 | super(Command, self).__init__(*args, **kwargs) |
55 | 55 | ||
@@ -68,18 +68,18 @@ class Command(BaseCommand, object): | @@ -68,18 +68,18 @@ class Command(BaseCommand, object): | ||
68 | 68 | ||
69 | Yield: An instance of `mailbox.mboxMessage` for each email in the | 69 | Yield: An instance of `mailbox.mboxMessage` for each email in the |
70 | file. | 70 | file. |
71 | - | 71 | + |
72 | """ | 72 | """ |
73 | self.log("Parsing email dump: %s." % email_filename) | 73 | self.log("Parsing email dump: %s." % email_filename) |
74 | mbox = mailbox.mbox(email_filename, factory=CustomMessage) | 74 | mbox = mailbox.mbox(email_filename, factory=CustomMessage) |
75 | - | 75 | + |
76 | # Get each email from mbox file | 76 | # Get each email from mbox file |
77 | # | 77 | # |
78 | # The following implementation was used because the object | 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 | # option but its performance was too poor. | 81 | # option but its performance was too poor. |
82 | - # | 82 | + # |
83 | #for message in tuple(mbox)[index:]: | 83 | #for message in tuple(mbox)[index:]: |
84 | # yield message | 84 | # yield message |
85 | # | 85 | # |
@@ -90,8 +90,8 @@ class Command(BaseCommand, object): | @@ -90,8 +90,8 @@ class Command(BaseCommand, object): | ||
90 | 90 | ||
91 | def get_emails(self, mailinglist_dir, all, exclude_lists): | 91 | def get_emails(self, mailinglist_dir, all, exclude_lists): |
92 | """Generator function that get the emails from each mailing | 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 | message previously imported. The lists set in `exclude_lists` | 95 | message previously imported. The lists set in `exclude_lists` |
96 | won't be imported. | 96 | won't be imported. |
97 | 97 | ||
@@ -99,20 +99,20 @@ class Command(BaseCommand, object): | @@ -99,20 +99,20 @@ class Command(BaseCommand, object): | ||
99 | 99 | ||
100 | """ | 100 | """ |
101 | self.log("Getting emails dumps from: %s" % mailinglist_dir) | 101 | self.log("Getting emails dumps from: %s" % mailinglist_dir) |
102 | - | 102 | + |
103 | # Get the list of directories ending with .mbox | 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 | if mbox.endswith('.mbox')) | 105 | if mbox.endswith('.mbox')) |
106 | - | 106 | + |
107 | # Get messages from each mbox | 107 | # Get messages from each mbox |
108 | for mbox in mailing_lists_mboxes: | 108 | for mbox in mailing_lists_mboxes: |
109 | mbox_path = os.path.join(mailinglist_dir, mbox, mbox) | 109 | mbox_path = os.path.join(mailinglist_dir, mbox, mbox) |
110 | mailinglist_name = mbox.split('.')[0] | 110 | mailinglist_name = mbox.split('.')[0] |
111 | - | 111 | + |
112 | # Check if the mailinglist is set not to be imported | 112 | # Check if the mailinglist is set not to be imported |
113 | if exclude_lists and mailinglist_name in exclude_lists: | 113 | if exclude_lists and mailinglist_name in exclude_lists: |
114 | continue | 114 | continue |
115 | - | 115 | + |
116 | # Find the index of the last imported message | 116 | # Find the index of the last imported message |
117 | if all: | 117 | if all: |
118 | n_msgs = 0 | 118 | n_msgs = 0 |
@@ -123,13 +123,13 @@ class Command(BaseCommand, object): | @@ -123,13 +123,13 @@ class Command(BaseCommand, object): | ||
123 | n_msgs = mailinglist.last_imported_index | 123 | n_msgs = mailinglist.last_imported_index |
124 | except MailingList.DoesNotExist: | 124 | except MailingList.DoesNotExist: |
125 | n_msgs = 0 | 125 | n_msgs = 0 |
126 | - | 126 | + |
127 | for index, msg in self.parse_emails(mbox_path, n_msgs): | 127 | for index, msg in self.parse_emails(mbox_path, n_msgs): |
128 | yield mailinglist_name, msg, index | 128 | yield mailinglist_name, msg, index |
129 | 129 | ||
130 | def get_thread(self, email, mailinglist): | 130 | def get_thread(self, email, mailinglist): |
131 | """Group messages by thread looking for similar subjects""" | 131 | """Group messages by thread looking for similar subjects""" |
132 | - | 132 | + |
133 | subject_slug = slugify(email.subject_clean) | 133 | subject_slug = slugify(email.subject_clean) |
134 | thread = self.THREAD_CACHE.get(subject_slug, {}).get(mailinglist.id) | 134 | thread = self.THREAD_CACHE.get(subject_slug, {}).get(mailinglist.id) |
135 | if thread is None: | 135 | if thread is None: |
@@ -137,27 +137,27 @@ class Command(BaseCommand, object): | @@ -137,27 +137,27 @@ class Command(BaseCommand, object): | ||
137 | mailinglist=mailinglist, | 137 | mailinglist=mailinglist, |
138 | subject_token=subject_slug | 138 | subject_token=subject_slug |
139 | )[0] | 139 | )[0] |
140 | - | 140 | + |
141 | if self.THREAD_CACHE.get(subject_slug) is None: | 141 | if self.THREAD_CACHE.get(subject_slug) is None: |
142 | self.THREAD_CACHE[subject_slug] = dict() | 142 | self.THREAD_CACHE[subject_slug] = dict() |
143 | self.THREAD_CACHE[subject_slug][mailinglist.id] = thread | 143 | self.THREAD_CACHE[subject_slug][mailinglist.id] = thread |
144 | 144 | ||
145 | thread.latest_message = email | 145 | thread.latest_message = email |
146 | - thread.save() | 146 | + thread.save() |
147 | return thread | 147 | return thread |
148 | - | 148 | + |
149 | def save_email(self, list_name, email_msg, index): | 149 | def save_email(self, list_name, email_msg, index): |
150 | """Save email message into the database.""" | 150 | """Save email message into the database.""" |
151 | - | 151 | + |
152 | # Update last imported message into the DB | 152 | # Update last imported message into the DB |
153 | mailinglist, created = MailingList.objects.get_or_create(name=list_name) | 153 | mailinglist, created = MailingList.objects.get_or_create(name=list_name) |
154 | mailinglist.last_imported_index = index | 154 | mailinglist.last_imported_index = index |
155 | - | ||
156 | - if created: | 155 | + |
156 | + if created: | ||
157 | # if the mailinglist is newly created it's sure that the message | 157 | # if the mailinglist is newly created it's sure that the message |
158 | # is not in the DB yet. | 158 | # is not in the DB yet. |
159 | self.create_email(mailinglist, email_msg) | 159 | self.create_email(mailinglist, email_msg) |
160 | - | 160 | + |
161 | else: | 161 | else: |
162 | # If the message is already at the database don't do anything | 162 | # If the message is already at the database don't do anything |
163 | try: | 163 | try: |
@@ -165,11 +165,11 @@ class Command(BaseCommand, object): | @@ -165,11 +165,11 @@ class Command(BaseCommand, object): | ||
165 | message_id=email_msg.get('Message-ID'), | 165 | message_id=email_msg.get('Message-ID'), |
166 | thread__mailinglist=mailinglist | 166 | thread__mailinglist=mailinglist |
167 | ) | 167 | ) |
168 | - | 168 | + |
169 | except Message.DoesNotExist: | 169 | except Message.DoesNotExist: |
170 | self.create_email(mailinglist, email_msg) | 170 | self.create_email(mailinglist, email_msg) |
171 | - | ||
172 | - mailinglist.save() | 171 | + |
172 | + mailinglist.save() | ||
173 | 173 | ||
174 | def create_email(self, mailinglist, email_msg): | 174 | def create_email(self, mailinglist, email_msg): |
175 | 175 | ||
@@ -198,59 +198,59 @@ class Command(BaseCommand, object): | @@ -198,59 +198,59 @@ class Command(BaseCommand, object): | ||
198 | email.thread = self.get_thread(email, mailinglist) | 198 | email.thread = self.get_thread(email, mailinglist) |
199 | email.save() | 199 | email.save() |
200 | 200 | ||
201 | - @transaction.commit_manually | 201 | + @transaction.commit_manually |
202 | def import_emails(self, archives_path, all, exclude_lists=None): | 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 | imported. The lists set in `exclude_lists` won't be imported. | 207 | imported. The lists set in `exclude_lists` won't be imported. |
208 | - | 208 | + |
209 | """ | 209 | """ |
210 | - | 210 | + |
211 | count = 0 | 211 | count = 0 |
212 | email_generator = self.get_emails(archives_path, all, exclude_lists) | 212 | email_generator = self.get_emails(archives_path, all, exclude_lists) |
213 | for mailinglist_name, msg, index in email_generator: | 213 | for mailinglist_name, msg, index in email_generator: |
214 | try: | 214 | try: |
215 | self.save_email(mailinglist_name, msg, index) | 215 | self.save_email(mailinglist_name, msg, index) |
216 | except: | 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 | # get stuck in case of errors. | 218 | # get stuck in case of errors. |
219 | transaction.rollback() | 219 | transaction.rollback() |
220 | raise | 220 | raise |
221 | - | 221 | + |
222 | count += 1 | 222 | count += 1 |
223 | if count % 1000 == 0: | 223 | if count % 1000 == 0: |
224 | transaction.commit() | 224 | transaction.commit() |
225 | - | 225 | + |
226 | transaction.commit() | 226 | transaction.commit() |
227 | - | 227 | + |
228 | def handle(self, *args, **options): | 228 | def handle(self, *args, **options): |
229 | """Main command method.""" | 229 | """Main command method.""" |
230 | - | 230 | + |
231 | lock_file = '/var/lock/colab/import_emails.lock' | 231 | lock_file = '/var/lock/colab/import_emails.lock' |
232 | - | 232 | + |
233 | # Already running, so quit | 233 | # Already running, so quit |
234 | if os.path.exists(lock_file): | 234 | if os.path.exists(lock_file): |
235 | self.log(("This script is already running. (If your are sure it's " | 235 | self.log(("This script is already running. (If your are sure it's " |
236 | "not please delete the lock file in %s')") % lock_file) | 236 | "not please delete the lock file in %s')") % lock_file) |
237 | sys.exit(0) | 237 | sys.exit(0) |
238 | - | 238 | + |
239 | if not os.path.exists(os.path.dirname(lock_file)): | 239 | if not os.path.exists(os.path.dirname(lock_file)): |
240 | os.mkdir(os.path.dirname(lock_file), 0755) | 240 | os.mkdir(os.path.dirname(lock_file), 0755) |
241 | - | 241 | + |
242 | run_lock = file(lock_file, 'w') | 242 | run_lock = file(lock_file, 'w') |
243 | run_lock.close() | 243 | run_lock.close() |
244 | - | 244 | + |
245 | archives_path = options.get('archives_path') | 245 | archives_path = options.get('archives_path') |
246 | self.log('Using archives_path `%s`' % self.default_archives_path) | 246 | self.log('Using archives_path `%s`' % self.default_archives_path) |
247 | - | 247 | + |
248 | if not os.path.exists(archives_path): | 248 | if not os.path.exists(archives_path): |
249 | raise CommandError('archives_path (%s) does not exist' % | 249 | raise CommandError('archives_path (%s) does not exist' % |
250 | archives_path) | 250 | archives_path) |
251 | - | ||
252 | - self.import_emails(archives_path, | 251 | + |
252 | + self.import_emails(archives_path, | ||
253 | options.get('all'), options.get('exclude_lists')) | 253 | options.get('all'), options.get('exclude_lists')) |
254 | - | 254 | + |
255 | os.remove(lock_file) | 255 | os.remove(lock_file) |
256 | - | 256 | + |
src/super_archives/models.py
@@ -79,6 +79,9 @@ class MailingList(models.Model): | @@ -79,6 +79,9 @@ class MailingList(models.Model): | ||
79 | logo = models.FileField(upload_to='list_logo') #TODO | 79 | logo = models.FileField(upload_to='list_logo') #TODO |
80 | last_imported_index = models.IntegerField(default=0) | 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 | def __unicode__(self): | 85 | def __unicode__(self): |
83 | return self.name | 86 | return self.name |
84 | 87 | ||
@@ -125,6 +128,10 @@ class Thread(models.Model): | @@ -125,6 +128,10 @@ class Thread(models.Model): | ||
125 | verbose_name_plural = _(u"Threads") | 128 | verbose_name_plural = _(u"Threads") |
126 | unique_together = ('subject_token', 'mailinglist') | 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 | def update_keywords(self): | 135 | def update_keywords(self): |
129 | blocks = MessageBlock.objects.filter(message__thread__pk=self.pk, | 136 | blocks = MessageBlock.objects.filter(message__thread__pk=self.pk, |
130 | is_reply=False) | 137 | is_reply=False) |
@@ -0,0 +1,55 @@ | @@ -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,18 +12,24 @@ | ||
12 | 12 | ||
13 | <h4>{% trans "Sort by" %}</h4> | 13 | <h4>{% trans "Sort by" %}</h4> |
14 | <ul class="unstyled-list"> | 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 | </ul> | 26 | </ul> |
21 | 27 | ||
22 | <h4>{% trans "Lists" %}</h4> | 28 | <h4>{% trans "Lists" %}</h4> |
23 | <ul class="unstyled-list"> | 29 | <ul class="unstyled-list"> |
24 | {% for list in lists %} | 30 | {% for list in lists %} |
25 | <li {% if list.name == selected_list %} title="{% trans "Remove filter" %}" class="selected" {% endif %}> | 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 | {% endifnotequal %}">{{ list.name }}</a></li> | 33 | {% endifnotequal %}">{{ list.name }}</a></li> |
28 | {% endfor %} | 34 | {% endfor %} |
29 | </ul> | 35 | </ul> |
src/super_archives/templates/search/indexes/super_archives/thread_description.txt
0 → 100644
@@ -0,0 +1,10 @@ | @@ -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 @@ | @@ -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,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 @@ | @@ -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 @@ | @@ -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,6 +3,7 @@ | ||
3 | import smtplib | 3 | import smtplib |
4 | 4 | ||
5 | from django import http | 5 | from django import http |
6 | +from django.conf import settings | ||
6 | from django.contrib import messages | 7 | from django.contrib import messages |
7 | from django.db import IntegrityError | 8 | from django.db import IntegrityError |
8 | from django.views.generic import View | 9 | from django.views.generic import View |
@@ -85,7 +86,7 @@ def list_messages(request): | @@ -85,7 +86,7 @@ def list_messages(request): | ||
85 | 'n_results': paginator.count, | 86 | 'n_results': paginator.count, |
86 | 'threads': threads, | 87 | 'threads': threads, |
87 | 'selected_list': selected_list, | 88 | 'selected_list': selected_list, |
88 | - 'order_by': order_by, | 89 | + 'order_data': settings.ORDERING_DATA, |
89 | } | 90 | } |
90 | return render(request, 'message-list.html', template_data) | 91 | return render(request, 'message-list.html', template_data) |
91 | 92 |
src/templates/base.html
@@ -111,7 +111,7 @@ | @@ -111,7 +111,7 @@ | ||
111 | {% endif %} | 111 | {% endif %} |
112 | </ul> | 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 | <div class="form-group"> | 115 | <div class="form-group"> |
116 | <label class="sr-only" for="header-searchbox">{% trans 'Search here...' %}</label> | 116 | <label class="sr-only" for="header-searchbox">{% trans 'Search here...' %}</label> |
117 | <input name="q" id="header-searchbox" | 117 | <input name="q" id="header-searchbox" |
src/templates/home.html
@@ -27,12 +27,12 @@ | @@ -27,12 +27,12 @@ | ||
27 | title="{% trans 'RSS - Latest collaborations' %}"> | 27 | title="{% trans 'RSS - Latest collaborations' %}"> |
28 | </a> | 28 | </a> |
29 | <ul class="message-list"> | 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 | {% endfor %} | 32 | {% endfor %} |
33 | </ul> | 33 | </ul> |
34 | <a class="column-align" | 34 | <a class="column-align" |
35 | - href="{% url 'search' %}?o=modified+desc"> | 35 | + href="{% url 'haystack_search' %}?order=latest"> |
36 | {% trans "View more collaborations..." %} | 36 | {% trans "View more collaborations..." %} |
37 | </a> | 37 | </a> |
38 | <div> </div> | 38 | <div> </div> |
@@ -0,0 +1,53 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 %} |