Commit 0f152d6a99d66a73238fe1784e341eeb40e74cce

Authored by Luan
2 parents d8b17800 9450256c

Merge branch 'master' into haystack

Conflicts:
	requirements.txt
	src/super_archives/models.py
puppet/modules/colab/manifests/init.pp
@@ -39,8 +39,8 @@ class colab { @@ -39,8 +39,8 @@ class colab {
39 } 39 }
40 40
41 supervisor::app { 'colab': 41 supervisor::app { 'colab':
42 - command => '/home/colab/.virtualenvs/colab/bin/gunicorn_django colab/src/colab',  
43 - directory => '/home/colab/', 42 + command => '/home/colab/.virtualenvs/colab/bin/gunicorn colab.wsgi:application -t 300',
  43 + directory => '/home/colab/colab/src/',
44 user => 'colab', 44 user => 'colab',
45 } 45 }
46 46
puppet/modules/colab/manifests/requirements.pp
@@ -6,6 +6,10 @@ class colab::requirements { @@ -6,6 +6,10 @@ class colab::requirements {
6 ensure => installed, 6 ensure => installed,
7 } 7 }
8 8
  9 + package { 'mercurial':
  10 + ensure => installed,
  11 + }
  12 +
9 # req to install python pkgs 13 # req to install python pkgs
10 package { 'python-pip': 14 package { 'python-pip':
11 ensure => installed, 15 ensure => installed,
requirements.txt
@@ -9,7 +9,9 @@ python-dateutil==1.5 @@ -9,7 +9,9 @@ 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 django-haystack==2.1
  12 +etiquetando==0.1
12 html2text 13 html2text
  14 +django-taggit
13 15
14 gunicorn 16 gunicorn
15 gevent 17 gevent
@@ -26,9 +28,9 @@ git+https://github.com/TracyWebTech/django-revproxy/ @@ -26,9 +28,9 @@ git+https://github.com/TracyWebTech/django-revproxy/
26 django-conversejs 28 django-conversejs
27 29
28 # Feedzilla (planet) and deps 30 # Feedzilla (planet) and deps
29 -feedzilla==0.22 31 +#feedzilla==0.22
  32 +hg+https://bitbucket.org/lorien/feedzilla
30 django-common 33 django-common
31 -django-taggit  
32 feedparser 34 feedparser
33 lxml 35 lxml
34 grab 36 grab
@@ -37,4 +39,3 @@ transliterate @@ -37,4 +39,3 @@ transliterate
37 # Diazo 39 # Diazo
38 #diazo 40 #diazo
39 git+https://github.com/TracyWebTech/diazo@escape_curly_brackets 41 git+https://github.com/TracyWebTech/diazo@escape_curly_brackets
40 -#lxml  
src/static/css/screen.css
@@ -335,6 +335,11 @@ ul.unstyled-list .glyphicon-chevron-right { @@ -335,6 +335,11 @@ ul.unstyled-list .glyphicon-chevron-right {
335 margin-top: 0; 335 margin-top: 0;
336 } 336 }
337 337
  338 +.tag-cloud {
  339 + text-align: justify;
  340 + margin-top: 6px;
  341 +}
  342 +
338 .tag-cloud .size-1 { 343 .tag-cloud .size-1 {
339 font-size: 0.9em; 344 font-size: 0.9em;
340 } 345 }
src/super_archives/migrations/0017_auto__add_keyword.py 0 → 100644
@@ -0,0 +1,151 @@ @@ -0,0 +1,151 @@
  1 +# -*- coding: utf-8 -*-
  2 +import datetime
  3 +from south.db import db
  4 +from south.v2 import SchemaMigration
  5 +from django.db import models
  6 +
  7 +
  8 +class Migration(SchemaMigration):
  9 +
  10 + def forwards(self, orm):
  11 + # Adding model 'Keyword'
  12 + db.create_table(u'super_archives_keyword', (
  13 + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
  14 + ('keyword', self.gf('django.db.models.fields.CharField')(max_length='128')),
  15 + ('weight', self.gf('django.db.models.fields.IntegerField')(default=0)),
  16 + ('thread', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['super_archives.Thread'])),
  17 + ))
  18 + db.send_create_signal(u'super_archives', ['Keyword'])
  19 +
  20 +
  21 + def backwards(self, orm):
  22 + # Deleting model 'Keyword'
  23 + db.delete_table(u'super_archives_keyword')
  24 +
  25 +
  26 + models = {
  27 + u'accounts.user': {
  28 + 'Meta': {'object_name': 'User'},
  29 + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
  30 + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
  31 + 'facebook': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  32 + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
  33 + 'google_talk': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
  34 + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
  35 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  36 + 'institution': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  37 + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
  38 + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  39 + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  40 + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
  41 + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
  42 + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
  43 + 'role': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  44 + 'twitter': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  45 + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
  46 + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
  47 + 'verification_hash': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
  48 + 'webpage': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'})
  49 + },
  50 + u'auth.group': {
  51 + 'Meta': {'object_name': 'Group'},
  52 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  53 + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
  54 + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
  55 + },
  56 + u'auth.permission': {
  57 + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
  58 + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  59 + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
  60 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  61 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
  62 + },
  63 + u'contenttypes.contenttype': {
  64 + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
  65 + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  66 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  67 + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  68 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
  69 + },
  70 + u'super_archives.emailaddress': {
  71 + 'Meta': {'ordering': "('id',)", 'object_name': 'EmailAddress'},
  72 + 'address': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
  73 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  74 + 'md5': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
  75 + 'real_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
  76 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['accounts.User']"})
  77 + },
  78 + u'super_archives.emailaddressvalidation': {
  79 + 'Meta': {'unique_together': "(('user', 'address'),)", 'object_name': 'EmailAddressValidation'},
  80 + 'address': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
  81 + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
  82 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  83 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails_not_validated'", 'null': 'True', 'to': u"orm['accounts.User']"}),
  84 + 'validation_key': ('django.db.models.fields.CharField', [], {'default': "'e2d18f1f8d6445fc91c04d17acdbe23a'", 'max_length': '32', 'null': 'True'})
  85 + },
  86 + u'super_archives.keyword': {
  87 + 'Meta': {'object_name': 'Keyword'},
  88 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  89 + 'keyword': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}),
  90 + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Thread']"}),
  91 + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
  92 + },
  93 + u'super_archives.mailinglist': {
  94 + 'Meta': {'object_name': 'MailingList'},
  95 + 'description': ('django.db.models.fields.TextField', [], {}),
  96 + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
  97 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  98 + 'last_imported_index': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  99 + 'logo': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
  100 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'})
  101 + },
  102 + u'super_archives.mailinglistmembership': {
  103 + 'Meta': {'object_name': 'MailingListMembership'},
  104 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  105 + 'mailinglist': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.MailingList']"}),
  106 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']"})
  107 + },
  108 + u'super_archives.message': {
  109 + 'Meta': {'ordering': "('received_time',)", 'unique_together': "(('thread', 'message_id'),)", 'object_name': 'Message'},
  110 + 'body': ('django.db.models.fields.TextField', [], {'default': "''"}),
  111 + 'from_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.EmailAddress']"}),
  112 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  113 + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
  114 + 'received_time': ('django.db.models.fields.DateTimeField', [], {}),
  115 + 'spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  116 + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}),
  117 + 'subject_clean': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}),
  118 + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Thread']", 'null': 'True'})
  119 + },
  120 + u'super_archives.messagemetadata': {
  121 + 'Message': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Message']"}),
  122 + 'Meta': {'object_name': 'MessageMetadata'},
  123 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  124 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
  125 + 'value': ('django.db.models.fields.TextField', [], {})
  126 + },
  127 + u'super_archives.pagehit': {
  128 + 'Meta': {'object_name': 'PageHit'},
  129 + 'hit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  130 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  131 + 'url_path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '2048', 'db_index': 'True'})
  132 + },
  133 + u'super_archives.thread': {
  134 + 'Meta': {'unique_together': "(('subject_token', 'mailinglist'),)", 'object_name': 'Thread'},
  135 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  136 + 'latest_message': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'null': 'True', 'to': u"orm['super_archives.Message']"}),
  137 + 'mailinglist': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.MailingList']"}),
  138 + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  139 + 'spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  140 + 'subject_token': ('django.db.models.fields.CharField', [], {'max_length': '512'})
  141 + },
  142 + u'super_archives.vote': {
  143 + 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'Vote'},
  144 + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
  145 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  146 + 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Message']"}),
  147 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']"})
  148 + }
  149 + }
  150 +
  151 + complete_apps = ['super_archives']
0 \ No newline at end of file 152 \ No newline at end of file
src/super_archives/migrations/0018_auto__add_index_message_received_time.py 0 → 100644
@@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
  1 +# -*- coding: utf-8 -*-
  2 +import datetime
  3 +from south.db import db
  4 +from south.v2 import SchemaMigration
  5 +from django.db import models
  6 +
  7 +
  8 +class Migration(SchemaMigration):
  9 +
  10 + def forwards(self, orm):
  11 + # Adding index on 'Message', fields ['received_time']
  12 + db.create_index(u'super_archives_message', ['received_time'])
  13 +
  14 +
  15 + def backwards(self, orm):
  16 + # Removing index on 'Message', fields ['received_time']
  17 + db.delete_index(u'super_archives_message', ['received_time'])
  18 +
  19 +
  20 + models = {
  21 + u'accounts.user': {
  22 + 'Meta': {'object_name': 'User'},
  23 + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
  24 + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
  25 + 'facebook': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  26 + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
  27 + 'google_talk': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
  28 + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
  29 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  30 + 'institution': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  31 + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
  32 + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  33 + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  34 + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
  35 + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
  36 + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
  37 + 'role': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  38 + 'twitter': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  39 + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
  40 + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
  41 + 'verification_hash': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
  42 + 'webpage': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'})
  43 + },
  44 + u'auth.group': {
  45 + 'Meta': {'object_name': 'Group'},
  46 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  47 + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
  48 + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
  49 + },
  50 + u'auth.permission': {
  51 + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
  52 + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  53 + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
  54 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  55 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
  56 + },
  57 + u'contenttypes.contenttype': {
  58 + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
  59 + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  60 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  61 + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  62 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
  63 + },
  64 + u'super_archives.emailaddress': {
  65 + 'Meta': {'ordering': "('id',)", 'object_name': 'EmailAddress'},
  66 + 'address': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
  67 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  68 + 'md5': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
  69 + 'real_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
  70 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['accounts.User']"})
  71 + },
  72 + u'super_archives.emailaddressvalidation': {
  73 + 'Meta': {'unique_together': "(('user', 'address'),)", 'object_name': 'EmailAddressValidation'},
  74 + 'address': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
  75 + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
  76 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  77 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails_not_validated'", 'null': 'True', 'to': u"orm['accounts.User']"}),
  78 + 'validation_key': ('django.db.models.fields.CharField', [], {'default': "'1775e2bdc5e341a9b9b8a6346eec1024'", 'max_length': '32', 'null': 'True'})
  79 + },
  80 + u'super_archives.keyword': {
  81 + 'Meta': {'ordering': "('?',)", 'object_name': 'Keyword'},
  82 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  83 + 'keyword': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}),
  84 + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Thread']"}),
  85 + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
  86 + },
  87 + u'super_archives.mailinglist': {
  88 + 'Meta': {'object_name': 'MailingList'},
  89 + 'description': ('django.db.models.fields.TextField', [], {}),
  90 + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
  91 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  92 + 'last_imported_index': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  93 + 'logo': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
  94 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'})
  95 + },
  96 + u'super_archives.mailinglistmembership': {
  97 + 'Meta': {'object_name': 'MailingListMembership'},
  98 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  99 + 'mailinglist': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.MailingList']"}),
  100 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']"})
  101 + },
  102 + u'super_archives.message': {
  103 + 'Meta': {'ordering': "('received_time',)", 'unique_together': "(('thread', 'message_id'),)", 'object_name': 'Message'},
  104 + 'body': ('django.db.models.fields.TextField', [], {'default': "''"}),
  105 + 'from_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.EmailAddress']"}),
  106 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  107 + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
  108 + 'received_time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
  109 + 'spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  110 + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}),
  111 + 'subject_clean': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}),
  112 + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Thread']", 'null': 'True'})
  113 + },
  114 + u'super_archives.messagemetadata': {
  115 + 'Message': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Message']"}),
  116 + 'Meta': {'object_name': 'MessageMetadata'},
  117 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  118 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
  119 + 'value': ('django.db.models.fields.TextField', [], {})
  120 + },
  121 + u'super_archives.pagehit': {
  122 + 'Meta': {'object_name': 'PageHit'},
  123 + 'hit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  124 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  125 + 'url_path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '2048', 'db_index': 'True'})
  126 + },
  127 + u'super_archives.thread': {
  128 + 'Meta': {'unique_together': "(('subject_token', 'mailinglist'),)", 'object_name': 'Thread'},
  129 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  130 + 'latest_message': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'null': 'True', 'to': u"orm['super_archives.Message']"}),
  131 + 'mailinglist': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.MailingList']"}),
  132 + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  133 + 'spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  134 + 'subject_token': ('django.db.models.fields.CharField', [], {'max_length': '512'})
  135 + },
  136 + u'super_archives.vote': {
  137 + 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'Vote'},
  138 + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
  139 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  140 + 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Message']"}),
  141 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']"})
  142 + }
  143 + }
  144 +
  145 + complete_apps = ['super_archives']
0 \ No newline at end of file 146 \ No newline at end of file
src/super_archives/migrations/0019_auto__add_messageblock.py 0 → 100644
@@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
  1 +# -*- coding: utf-8 -*-
  2 +import datetime
  3 +from south.db import db
  4 +from south.v2 import SchemaMigration
  5 +from django.db import models
  6 +
  7 +
  8 +class Migration(SchemaMigration):
  9 +
  10 + def forwards(self, orm):
  11 + # Adding model 'MessageBlock'
  12 + db.create_table(u'super_archives_messageblock', (
  13 + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
  14 + ('message', self.gf('django.db.models.fields.related.ForeignKey')(related_name='blocks', to=orm['super_archives.Message'])),
  15 + ('text', self.gf('django.db.models.fields.TextField')()),
  16 + ('is_reply', self.gf('django.db.models.fields.BooleanField')()),
  17 + ('order', self.gf('django.db.models.fields.IntegerField')()),
  18 + ))
  19 + db.send_create_signal(u'super_archives', ['MessageBlock'])
  20 +
  21 +
  22 + def backwards(self, orm):
  23 + # Deleting model 'MessageBlock'
  24 + db.delete_table(u'super_archives_messageblock')
  25 +
  26 +
  27 + models = {
  28 + u'accounts.user': {
  29 + 'Meta': {'object_name': 'User'},
  30 + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
  31 + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
  32 + 'facebook': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  33 + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
  34 + 'google_talk': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
  35 + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
  36 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  37 + 'institution': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  38 + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
  39 + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  40 + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  41 + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
  42 + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
  43 + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
  44 + 'role': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  45 + 'twitter': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
  46 + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
  47 + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
  48 + 'verification_hash': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
  49 + 'webpage': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'})
  50 + },
  51 + u'auth.group': {
  52 + 'Meta': {'object_name': 'Group'},
  53 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  54 + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
  55 + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
  56 + },
  57 + u'auth.permission': {
  58 + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
  59 + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  60 + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
  61 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  62 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
  63 + },
  64 + u'contenttypes.contenttype': {
  65 + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
  66 + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  67 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  68 + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
  69 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
  70 + },
  71 + u'super_archives.emailaddress': {
  72 + 'Meta': {'ordering': "('id',)", 'object_name': 'EmailAddress'},
  73 + 'address': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
  74 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  75 + 'md5': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
  76 + 'real_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
  77 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['accounts.User']"})
  78 + },
  79 + u'super_archives.emailaddressvalidation': {
  80 + 'Meta': {'unique_together': "(('user', 'address'),)", 'object_name': 'EmailAddressValidation'},
  81 + 'address': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
  82 + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
  83 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  84 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emails_not_validated'", 'null': 'True', 'to': u"orm['accounts.User']"}),
  85 + 'validation_key': ('django.db.models.fields.CharField', [], {'default': "'f7c4c15797f34834bf5a8b9bd84fabee'", 'max_length': '32', 'null': 'True'})
  86 + },
  87 + u'super_archives.keyword': {
  88 + 'Meta': {'ordering': "('?',)", 'object_name': 'Keyword'},
  89 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  90 + 'keyword': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}),
  91 + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Thread']"}),
  92 + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
  93 + },
  94 + u'super_archives.mailinglist': {
  95 + 'Meta': {'object_name': 'MailingList'},
  96 + 'description': ('django.db.models.fields.TextField', [], {}),
  97 + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
  98 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  99 + 'last_imported_index': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  100 + 'logo': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
  101 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'})
  102 + },
  103 + u'super_archives.mailinglistmembership': {
  104 + 'Meta': {'object_name': 'MailingListMembership'},
  105 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  106 + 'mailinglist': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.MailingList']"}),
  107 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']"})
  108 + },
  109 + u'super_archives.message': {
  110 + 'Meta': {'ordering': "('received_time',)", 'unique_together': "(('thread', 'message_id'),)", 'object_name': 'Message'},
  111 + 'body': ('django.db.models.fields.TextField', [], {'default': "''"}),
  112 + 'from_address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.EmailAddress']"}),
  113 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  114 + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
  115 + 'received_time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
  116 + 'spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  117 + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}),
  118 + 'subject_clean': ('django.db.models.fields.CharField', [], {'max_length': '512', 'db_index': 'True'}),
  119 + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Thread']", 'null': 'True'})
  120 + },
  121 + u'super_archives.messageblock': {
  122 + 'Meta': {'ordering': "('order',)", 'object_name': 'MessageBlock'},
  123 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  124 + 'is_reply': ('django.db.models.fields.BooleanField', [], {}),
  125 + 'message': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blocks'", 'to': u"orm['super_archives.Message']"}),
  126 + 'order': ('django.db.models.fields.IntegerField', [], {}),
  127 + 'text': ('django.db.models.fields.TextField', [], {})
  128 + },
  129 + u'super_archives.messagemetadata': {
  130 + 'Message': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Message']"}),
  131 + 'Meta': {'object_name': 'MessageMetadata'},
  132 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  133 + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
  134 + 'value': ('django.db.models.fields.TextField', [], {})
  135 + },
  136 + u'super_archives.pagehit': {
  137 + 'Meta': {'object_name': 'PageHit'},
  138 + 'hit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  139 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  140 + 'url_path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '2048', 'db_index': 'True'})
  141 + },
  142 + u'super_archives.thread': {
  143 + 'Meta': {'unique_together': "(('subject_token', 'mailinglist'),)", 'object_name': 'Thread'},
  144 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  145 + 'latest_message': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'unique': 'True', 'null': 'True', 'to': u"orm['super_archives.Message']"}),
  146 + 'mailinglist': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.MailingList']"}),
  147 + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
  148 + 'spam': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
  149 + 'subject_token': ('django.db.models.fields.CharField', [], {'max_length': '512'})
  150 + },
  151 + u'super_archives.vote': {
  152 + 'Meta': {'unique_together': "(('user', 'message'),)", 'object_name': 'Vote'},
  153 + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
  154 + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
  155 + 'message': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['super_archives.Message']"}),
  156 + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['accounts.User']"})
  157 + }
  158 + }
  159 +
  160 + complete_apps = ['super_archives']
0 \ No newline at end of file 161 \ No newline at end of file
src/super_archives/models.py
@@ -6,10 +6,17 @@ from hashlib import md5 @@ -6,10 +6,17 @@ from hashlib import md5
6 from django.db import models 6 from django.db import models
7 from django.conf import settings 7 from django.conf import settings
8 from django.utils import timezone 8 from django.utils import timezone
  9 +from django.core.cache import cache
9 from django.contrib.auth import get_user_model 10 from django.contrib.auth import get_user_model
10 from django.core.urlresolvers import reverse, NoReverseMatch 11 from django.core.urlresolvers import reverse, NoReverseMatch
11 from django.utils.translation import ugettext_lazy as _ 12 from django.utils.translation import ugettext_lazy as _
12 13
  14 +from html2text import html2text
  15 +from taggit.managers import TaggableManager
  16 +
  17 +from .utils import blocks
  18 +from .utils.etiquetador import etiquetador
  19 +
13 20
14 User = get_user_model() 21 User = get_user_model()
15 22
@@ -87,6 +94,18 @@ class MailingListMembership(models.Model): @@ -87,6 +94,18 @@ class MailingListMembership(models.Model):
87 return '%s on %s' % (self.user.email, self.mailinglist.name) 94 return '%s on %s' % (self.user.email, self.mailinglist.name)
88 95
89 96
  97 +class Keyword(models.Model):
  98 + keyword = models.CharField(max_length='128')
  99 + weight = models.IntegerField(default=0)
  100 + thread = models.ForeignKey('Thread')
  101 +
  102 + class Meta:
  103 + ordering = ('?', ) # random order
  104 +
  105 + def __unicode__(self):
  106 + return self.keyword
  107 +
  108 +
90 class Thread(models.Model): 109 class Thread(models.Model):
91 110
92 subject_token = models.CharField(max_length=512) 111 subject_token = models.CharField(max_length=512)
@@ -102,6 +121,7 @@ class Thread(models.Model): @@ -102,6 +121,7 @@ class Thread(models.Model):
102 121
103 all_objects = models.Manager() 122 all_objects = models.Manager()
104 objects = NotSpamManager() 123 objects = NotSpamManager()
  124 + tags = TaggableManager()
105 125
106 class Meta: 126 class Meta:
107 verbose_name = _(u"Thread") 127 verbose_name = _(u"Thread")
@@ -112,6 +132,35 @@ class Thread(models.Model): @@ -112,6 +132,35 @@ class Thread(models.Model):
112 def get_absolute_url(self): 132 def get_absolute_url(self):
113 return ('thread_view', [self.mailinglist, self.subject_token]) 133 return ('thread_view', [self.mailinglist, self.subject_token])
114 134
  135 + def update_keywords(self):
  136 + blocks = MessageBlock.objects.filter(message__thread__pk=self.pk,
  137 + is_reply=False)
  138 +
  139 + self.tags.clear()
  140 +
  141 + text = u'\n'.join(map(unicode, blocks))
  142 + tags = etiquetador(html2text(text))
  143 +
  144 + for tag, weight in tags:
  145 + keyword, created = Keyword.objects.get_or_create(thread=self,
  146 + keyword=tag)
  147 + if created or keyword.weight != weight:
  148 + keyword.weight = weight
  149 + keyword.save()
  150 +
  151 + if weight >= 3:
  152 + self.tags.add(tag)
  153 +
  154 + # removing old tags not used anylonger
  155 + if tags:
  156 + qs = Keyword.objects.filter(thread=self)
  157 + qs = qs.exclude(keyword__in=zip(*tags)[0])
  158 + qs.delete()
  159 +
  160 + def save(self, *args, **kwargs):
  161 + super(Thread, self).save(*args, **kwargs)
  162 + self.update_keywords()
  163 +
115 def __unicode__(self): 164 def __unicode__(self):
116 return '%s - %s (%s)' % (self.id, 165 return '%s - %s (%s)' % (self.id,
117 self.subject_token, 166 self.subject_token,
@@ -197,7 +246,7 @@ class Message(models.Model): @@ -197,7 +246,7 @@ class Message(models.Model):
197 body = models.TextField(default='', 246 body = models.TextField(default='',
198 verbose_name=_(u"Message body"), 247 verbose_name=_(u"Message body"),
199 help_text=_(u"Please enter a message body")) 248 help_text=_(u"Please enter a message body"))
200 - received_time = models.DateTimeField() 249 + received_time = models.DateTimeField(db_index=True)
201 message_id = models.CharField(max_length=512) 250 message_id = models.CharField(max_length=512)
202 spam = models.BooleanField(default=False) 251 spam = models.BooleanField(default=False)
203 252
@@ -208,12 +257,20 @@ class Message(models.Model): @@ -208,12 +257,20 @@ class Message(models.Model):
208 verbose_name = _(u"Message") 257 verbose_name = _(u"Message")
209 verbose_name_plural = _(u"Messages") 258 verbose_name_plural = _(u"Messages")
210 unique_together = ('thread', 'message_id') 259 unique_together = ('thread', 'message_id')
  260 + ordering = ('received_time', )
211 261
212 def __unicode__(self): 262 def __unicode__(self):
213 return '(%s) %s: %s' % (self.id, 263 return '(%s) %s: %s' % (self.id,
214 self.from_address.get_full_name(), 264 self.from_address.get_full_name(),
215 self.subject_clean) 265 self.subject_clean)
216 266
  267 + def update_blocks(self):
  268 + # delete all blocks for that message
  269 + self.blocks.all().delete()
  270 +
  271 + for i, block in enumerate(blocks.EmailBlockParser(self)):
  272 + MessageBlock.from_emailblock(block, self, i)
  273 +
217 @property 274 @property
218 def mailinglist(self): 275 def mailinglist(self):
219 if not self.thread or not self.thread.mailinglist: 276 if not self.thread or not self.thread.mailinglist:
@@ -262,6 +319,28 @@ class Message(models.Model): @@ -262,6 +319,28 @@ class Message(models.Model):
262 return self.received_time 319 return self.received_time
263 320
264 321
  322 +class MessageBlock(models.Model):
  323 + message = models.ForeignKey(Message, related_name='blocks')
  324 + text = models.TextField()
  325 + is_reply = models.BooleanField()
  326 + order = models.IntegerField()
  327 +
  328 + def __unicode__(self):
  329 + return self.text
  330 +
  331 + class Meta:
  332 + ordering = ('order', )
  333 +
  334 + @classmethod
  335 + def from_emailblock(klass, emailblock, message, order):
  336 + obj = klass.objects.create(text=emailblock.text,
  337 + is_reply=emailblock.is_reply,
  338 + message=message,
  339 + order=order)
  340 + return obj
  341 +
  342 +
  343 +
265 class MessageMetadata(models.Model): 344 class MessageMetadata(models.Model):
266 Message = models.ForeignKey(Message) 345 Message = models.ForeignKey(Message)
267 # Same problem here than on subjects. Read comment above 346 # Same problem here than on subjects. Read comment above
src/super_archives/templates/message-thread.html
@@ -96,7 +96,7 @@ @@ -96,7 +96,7 @@
96 <hr /> 96 <hr />
97 </div> 97 </div>
98 98
99 - <div class="col-lg-10 col-md-10 col-sm-12"> 99 + <div class="col-lg-9 col-md-9 col-sm-12">
100 <ul class="unstyled-list"> 100 <ul class="unstyled-list">
101 {% for email in emails %} 101 {% for email in emails %}
102 {% with email.from_address.user.get_absolute_url as profile_link %} 102 {% with email.from_address.user.get_absolute_url as profile_link %}
@@ -140,7 +140,7 @@ @@ -140,7 +140,7 @@
140 </div> 140 </div>
141 <div class="panel-collapse in"> 141 <div class="panel-collapse in">
142 <div class="panel-body"> 142 <div class="panel-body">
143 - {% display_message email emails %} 143 + {% display_message email %}
144 </div> 144 </div>
145 </div> 145 </div>
146 </div> 146 </div>
@@ -152,7 +152,7 @@ @@ -152,7 +152,7 @@
152 </ul> 152 </ul>
153 </div> 153 </div>
154 154
155 - <div class="col-lg-2 col-md-2 hidden-sm hidden-xs"> 155 + <div class="col-lg-3 col-md-3 hidden-sm hidden-xs">
156 <h4><strong>{% trans "Order by" %}:</strong></h4> 156 <h4><strong>{% trans "Order by" %}:</strong></h4>
157 <ul class="unstyled-list"> 157 <ul class="unstyled-list">
158 <li> 158 <li>
@@ -165,8 +165,21 @@ @@ -165,8 +165,21 @@
165 </li> 165 </li>
166 </ul> 166 </ul>
167 167
168 - <h4><strong>{% trans "Statistics:" %}</strong></h4> 168 + {% if thread.tags.similar_objects %}
  169 + <h4><strong>{% trans "Releated:" %}</strong></h4>
  170 + <ul class="unstyled-list">
  171 + {% for similar in thread.tags.similar_objects|slice:":10" %}
  172 + {% with similar.message_set.first as message %}
  173 + <li>
  174 + <span class="label label-primary label-small">{{ similar.mailinglist.name }}</span>
  175 + <a href="{% url 'thread_view' similar.mailinglist.name similar.subject_token %}">{{ message.subject_clean|truncatechars:50 }}</a>
  176 + </li>
  177 + {% endwith %}
  178 + {% endfor %}
  179 + </ul>
  180 + {% endif %}
169 181
  182 + <h4><strong>{% trans "Statistics:" %}</strong></h4>
170 <ul class="unstyled-list"> 183 <ul class="unstyled-list">
171 <li> 184 <li>
172 <span class="glyphicon glyphicon-chevron-right"></span> 185 <span class="glyphicon glyphicon-chevron-right"></span>
@@ -190,6 +203,16 @@ @@ -190,6 +203,16 @@
190 <h5>{{ total_votes }} {% trans "times" %}</h5> 203 <h5>{{ total_votes }} {% trans "times" %}</h5>
191 </li> 204 </li>
192 </ul> 205 </ul>
  206 +
  207 + {% if thread.keyword_set.count %}
  208 + <h4><strong>{% trans "Tags:" %}</strong></h4>
  209 + <div class="tag-cloud">
  210 + {% for keyword in thread.keyword_set.all %}
  211 + <a class="tag size-{{ keyword.weight }}" href="#">{{ keyword|escape }}</a>
  212 + {% endfor %}
  213 + </div>
  214 + {% endif %}
  215 +
193 </div> 216 </div>
194 217
195 </div> 218 </div>
src/super_archives/templates/superarchives/tags/display_message.html
1 -{% load cache %}  
2 -  
3 -{% cache cache_timeout 'display_message' cache_key %}  
4 -{% for message, class in messages %}  
5 - {% if class == 'reply' %} 1 +{% for block in blocks %}
  2 + {% if block.is_reply %}
6 <button class="btn btn-info btn-xs" onclick="$(this).next().toggle();">...</button> 3 <button class="btn btn-info btn-xs" onclick="$(this).next().toggle();">...</button>
7 - <div style="display: none; color: #707";>{% else %}<div>{% endif %}{{ message|safe|linebreaksbr }}</div> 4 + <div style="display: none; color: #707;">{% else %}<div>{% endif %}{{ block|safe|linebreaksbr }}</div>
8 {% endfor %} 5 {% endfor %}
9 -{% endcache %}  
src/super_archives/templatetags/superarchives.py
1 1
2 -import re  
3 -  
4 from django import template 2 from django import template
5 -from django.core.cache import cache  
6 -  
7 -from html2text import html2text  
8 3
9 4
10 register = template.Library() 5 register = template.Library()
11 TEMPLATE_PATH = 'superarchives/tags/' 6 TEMPLATE_PATH = 'superarchives/tags/'
12 7
13 8
14 -EXTENDED_PUNCTUATION = '!"#$%&\'()*+,-./:;=?@[\\]^_`{|}~ \t\n\r\x0b\x0c'  
15 -RE_WRAPPED_BY_HTML = re.compile(r'^<[a-z]+[^>]*>.*</[a-z]+[^>]*>$',  
16 - re.MULTILINE|re.IGNORECASE|re.DOTALL)  
17 -RE_LINKS = re.compile(r'(?P<link>https?://[^ \t\r\n\<]+)')  
18 -  
19 -  
20 -def find_links(block):  
21 - links = RE_LINKS.finditer(block)  
22 -  
23 - block, n = RE_LINKS.subn(r'<a target="_blank" href="\g<link>">\g<link></a>',  
24 - block)  
25 -  
26 - return block  
27 -  
28 -  
29 -def join(block):  
30 - block_txt = u''.join(block)  
31 -  
32 - if RE_WRAPPED_BY_HTML.match(block_txt.strip()):  
33 - block = html2text(block_txt)  
34 - else:  
35 - block = block_txt  
36 -  
37 - return find_links(block)  
38 -  
39 -def is_reply(line, message, thread):  
40 - clean_line = line.strip()  
41 - if clean_line.startswith('>'):  
42 - return True  
43 -  
44 - for other_msg in thread:  
45 - if other_msg == message:  
46 - return False  
47 -  
48 - clean_body = other_msg.body.replace('\r', ' ')\  
49 - .replace('\n', ' ')  
50 - if clean_line.strip('> ') in clean_body:  
51 - return True  
52 -  
53 - return False  
54 -  
55 -  
56 @register.inclusion_tag(TEMPLATE_PATH + 'display_message.html') 9 @register.inclusion_tag(TEMPLATE_PATH + 'display_message.html')
57 -def display_message(email, thread):  
58 - message = email.body  
59 - messages = []  
60 -  
61 - block = []  
62 - reply_block = False  
63 -  
64 - for line in message.split('\n'):  
65 - if line.strip(EXTENDED_PUNCTUATION):  
66 - if is_reply(line, email, thread):  
67 - if not reply_block:  
68 - reply_block = True  
69 - messages.append((join(block), 'normal'))  
70 - block = []  
71 - elif reply_block:  
72 - reply_block = False  
73 - messages.append((join(block), 'reply'))  
74 - block = []  
75 -  
76 - block.append(line.rstrip() + '\n')  
77 -  
78 - if reply_block:  
79 - messages.append((join(block), 'reply'))  
80 - else:  
81 - messages.append((join(block), 'normal')) 10 +def display_message(email):
  11 + if not email.blocks.count():
  12 + email.update_blocks()
82 13
83 - return {'messages': messages,  
84 - 'cache_key': email.pk,  
85 - 'cache_timeout': cache.default_timeout} 14 + return { 'blocks': email.blocks.all }
src/super_archives/utils.py
@@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
1 -  
2 -from django.core import mail  
3 -from django.conf import settings  
4 -from django.template import Context, loader  
5 -from django.utils.translation import ugettext as _  
6 -  
7 -  
8 -def colab_send_email(subject, message, to):  
9 - from_email = settings.COLAB_FROM_ADDRESS  
10 - return mail.send_mail(subject, message, from_email, [to])  
11 -  
12 -  
13 -def send_verification_email(to, user, validation_key):  
14 - subject = _('Please verify your email ') + u'{}'.format(to)  
15 - msg_tmpl = loader.get_template('superarchives/emails/email_verification.txt')  
16 - message = msg_tmpl.render(Context({'to': to, 'user': user,  
17 - 'key': validation_key,  
18 - 'SITE_URL': settings.SITE_URL}))  
19 - return colab_send_email(subject, message, to)  
src/super_archives/utils/__init__.py 0 → 100644
src/super_archives/utils/blocks.py 0 → 100644
@@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
  1 +
  2 +import re
  3 +
  4 +from django.utils.html import strip_tags
  5 +
  6 +from html2text import html2text
  7 +
  8 +
  9 +EXTENDED_PUNCTUATION = '!"#$%&\'()*+,-./:;=?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
  10 +RE_WRAPPED_BY_HTML = re.compile(r'^<[a-z]+[^>]*>.*</[a-z]+[^>]*>$',
  11 + re.MULTILINE|re.IGNORECASE|re.DOTALL)
  12 +RE_LINKS = re.compile(r'(?P<link>https?://[^ \t\r\n\<]+)')
  13 +LINK_MARKUP = u'<a target="_blank" href="\g<link>">\g<link></a>'
  14 +
  15 +RE_REPLY_LINE = re.compile(r'^[\s\t>]*>[\s\t]*')
  16 +
  17 +RE_BR_TO_LINEBREAK = re.compile(r'<\s*/?\s*br\s*/?\s*>')
  18 +
  19 +
  20 +class EmailBlock(list):
  21 + def __init__(self, is_reply=False, mark_links=True, html2text=True):
  22 + self.mark_links = mark_links
  23 + self.html2text = html2text
  24 + self.is_reply = is_reply
  25 +
  26 + def _html2text(self, text):
  27 + if RE_WRAPPED_BY_HTML.match(text.strip()):
  28 + return html2text(text)
  29 +
  30 + text, n = RE_BR_TO_LINEBREAK.subn('\n', text)
  31 + text = strip_tags(text)
  32 + return text
  33 +
  34 + def _mark_links(self, text):
  35 + text, n = RE_LINKS.subn(LINK_MARKUP, text)
  36 + return text
  37 +
  38 + @property
  39 + def text(self):
  40 + block = u''.join(self)
  41 +
  42 + if self.html2text:
  43 + block = self._html2text(block)
  44 +
  45 + if self.mark_links:
  46 + block = self._mark_links(block)
  47 +
  48 + return block
  49 +
  50 + def __unicode__(self):
  51 + return self.text
  52 +
  53 + def __str__(self):
  54 + return self.text
  55 +
  56 +
  57 +class EmailBlockParser(list):
  58 + def __init__(self, email):
  59 + self.email = email
  60 + self.thread_emails = email.thread.message_set
  61 +
  62 + message = email.body
  63 + block = EmailBlock()
  64 +
  65 + for line in message.split('\n'):
  66 + if self.context_switch(line, block):
  67 + self.append(block)
  68 + new_block_context = not block.is_reply
  69 + block = EmailBlock(is_reply=new_block_context)
  70 +
  71 + block.append(line + '\n')
  72 +
  73 + self.append(block)
  74 +
  75 + def context_switch(self, line, block):
  76 + if line.strip(EXTENDED_PUNCTUATION):
  77 + if self.is_reply(line):
  78 + if not block.is_reply:
  79 + return True
  80 +
  81 + else:
  82 + if block.is_reply:
  83 + return True
  84 +
  85 + return False
  86 +
  87 + def is_reply(self, line):
  88 + stripped_line = line.strip()
  89 + if stripped_line.startswith('>') or RE_REPLY_LINE.match(line):
  90 + return True
  91 +
  92 + clean_line = RE_REPLY_LINE.subn('', stripped_line)[0]
  93 + queryset = self.thread_emails.filter(
  94 + received_time__lt=self.email.received_time,
  95 + body__contains=clean_line).order_by('-received_time')
  96 +
  97 + if queryset[:1]:
  98 + return True
  99 +
  100 + return False
src/super_archives/utils/email.py 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +
  2 +from django.core import mail
  3 +from django.conf import settings
  4 +from django.template import Context, loader
  5 +from django.utils.translation import ugettext as _
  6 +
  7 +
  8 +def colab_send_email(subject, message, to):
  9 + from_email = settings.COLAB_FROM_ADDRESS
  10 + return mail.send_mail(subject, message, from_email, [to])
  11 +
  12 +
  13 +def send_verification_email(to, user, validation_key):
  14 + subject = _('Please verify your email ') + u'{}'.format(to)
  15 + msg_tmpl = loader.get_template('superarchives/emails/email_verification.txt')
  16 + message = msg_tmpl.render(Context({'to': to, 'user': user,
  17 + 'key': validation_key,
  18 + 'SITE_URL': settings.SITE_URL}))
  19 + return colab_send_email(subject, message, to)
src/super_archives/utils/etiquetador.py 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +
  2 +from etiquetando import Etiquetador
  3 +
  4 +etiquetador = Etiquetador(word_min_size=3, max_tags=24,
  5 + weight_range=(1, 4))
src/super_archives/views.py
@@ -14,8 +14,8 @@ from django.contrib.auth.decorators import login_required @@ -14,8 +14,8 @@ from django.contrib.auth.decorators import login_required
14 from django.shortcuts import render, redirect 14 from django.shortcuts import render, redirect
15 15
16 from . import queries 16 from . import queries
17 -from .utils import send_verification_email  
18 from .decorators import count_hit 17 from .decorators import count_hit
  18 +from .utils.email import send_verification_email
19 from .models import MailingList, Thread, EmailAddress, EmailAddressValidation 19 from .models import MailingList, Thread, EmailAddress, EmailAddressValidation
20 20
21 21
@@ -42,18 +42,19 @@ def thread(request, mailinglist, thread_token): @@ -42,18 +42,19 @@ def thread(request, mailinglist, thread_token):
42 total_votes += email.votes_count() 42 total_votes += email.votes_count()
43 43
44 # Update relevance score 44 # Update relevance score
45 - query = Thread.objects.filter(mailinglist__name=mailinglist)  
46 - thread = query.get(subject_token=thread_token) 45 + thread = Thread.objects.get(subject_token=thread_token,
  46 + mailinglist__name=mailinglist)
47 thread.update_score() 47 thread.update_score()
48 48
49 - template_data = { 49 + context = {
50 'first_msg': first_message, 50 'first_msg': first_message,
51 'emails': [first_message] + list(emails), 51 'emails': [first_message] + list(emails),
52 'pagehits': queries.get_page_hits(request.path_info), 52 'pagehits': queries.get_page_hits(request.path_info),
53 'total_votes': total_votes, 53 'total_votes': total_votes,
  54 + 'thread': thread,
54 } 55 }
55 56
56 - return render(request, 'message-thread.html', template_data) 57 + return render(request, 'message-thread.html', context)
57 58
58 59
59 def list_messages(request): 60 def list_messages(request):