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 39 }
40 40  
41 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 44 user => 'colab',
45 45 }
46 46  
... ...
puppet/modules/colab/manifests/requirements.pp
... ... @@ -6,6 +6,10 @@ class colab::requirements {
6 6 ensure => installed,
7 7 }
8 8  
  9 + package { 'mercurial':
  10 + ensure => installed,
  11 + }
  12 +
9 13 # req to install python pkgs
10 14 package { 'python-pip':
11 15 ensure => installed,
... ...
requirements.txt
... ... @@ -9,7 +9,9 @@ python-dateutil==1.5
9 9 django-cliauth==0.9
10 10 django-mobile==0.3.0
11 11 django-haystack==2.1
  12 +etiquetando==0.1
12 13 html2text
  14 +django-taggit
13 15  
14 16 gunicorn
15 17 gevent
... ... @@ -26,9 +28,9 @@ git+https://github.com/TracyWebTech/django-revproxy/
26 28 django-conversejs
27 29  
28 30 # Feedzilla (planet) and deps
29   -feedzilla==0.22
  31 +#feedzilla==0.22
  32 +hg+https://bitbucket.org/lorien/feedzilla
30 33 django-common
31   -django-taggit
32 34 feedparser
33 35 lxml
34 36 grab
... ... @@ -37,4 +39,3 @@ transliterate
37 39 # Diazo
38 40 #diazo
39 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 335 margin-top: 0;
336 336 }
337 337  
  338 +.tag-cloud {
  339 + text-align: justify;
  340 + margin-top: 6px;
  341 +}
  342 +
338 343 .tag-cloud .size-1 {
339 344 font-size: 0.9em;
340 345 }
... ...
src/super_archives/migrations/0017_auto__add_keyword.py 0 → 100644
... ... @@ -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 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 @@
  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 146 \ No newline at end of file
... ...
src/super_archives/migrations/0019_auto__add_messageblock.py 0 → 100644
... ... @@ -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 161 \ No newline at end of file
... ...
src/super_archives/models.py
... ... @@ -6,10 +6,17 @@ from hashlib import md5
6 6 from django.db import models
7 7 from django.conf import settings
8 8 from django.utils import timezone
  9 +from django.core.cache import cache
9 10 from django.contrib.auth import get_user_model
10 11 from django.core.urlresolvers import reverse, NoReverseMatch
11 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 21 User = get_user_model()
15 22  
... ... @@ -87,6 +94,18 @@ class MailingListMembership(models.Model):
87 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 109 class Thread(models.Model):
91 110  
92 111 subject_token = models.CharField(max_length=512)
... ... @@ -102,6 +121,7 @@ class Thread(models.Model):
102 121  
103 122 all_objects = models.Manager()
104 123 objects = NotSpamManager()
  124 + tags = TaggableManager()
105 125  
106 126 class Meta:
107 127 verbose_name = _(u"Thread")
... ... @@ -112,6 +132,35 @@ class Thread(models.Model):
112 132 def get_absolute_url(self):
113 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 164 def __unicode__(self):
116 165 return '%s - %s (%s)' % (self.id,
117 166 self.subject_token,
... ... @@ -197,7 +246,7 @@ class Message(models.Model):
197 246 body = models.TextField(default='',
198 247 verbose_name=_(u"Message body"),
199 248 help_text=_(u"Please enter a message body"))
200   - received_time = models.DateTimeField()
  249 + received_time = models.DateTimeField(db_index=True)
201 250 message_id = models.CharField(max_length=512)
202 251 spam = models.BooleanField(default=False)
203 252  
... ... @@ -208,12 +257,20 @@ class Message(models.Model):
208 257 verbose_name = _(u"Message")
209 258 verbose_name_plural = _(u"Messages")
210 259 unique_together = ('thread', 'message_id')
  260 + ordering = ('received_time', )
211 261  
212 262 def __unicode__(self):
213 263 return '(%s) %s: %s' % (self.id,
214 264 self.from_address.get_full_name(),
215 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 274 @property
218 275 def mailinglist(self):
219 276 if not self.thread or not self.thread.mailinglist:
... ... @@ -262,6 +319,28 @@ class Message(models.Model):
262 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 344 class MessageMetadata(models.Model):
266 345 Message = models.ForeignKey(Message)
267 346 # Same problem here than on subjects. Read comment above
... ...
src/super_archives/templates/message-thread.html
... ... @@ -96,7 +96,7 @@
96 96 <hr />
97 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 100 <ul class="unstyled-list">
101 101 {% for email in emails %}
102 102 {% with email.from_address.user.get_absolute_url as profile_link %}
... ... @@ -140,7 +140,7 @@
140 140 </div>
141 141 <div class="panel-collapse in">
142 142 <div class="panel-body">
143   - {% display_message email emails %}
  143 + {% display_message email %}
144 144 </div>
145 145 </div>
146 146 </div>
... ... @@ -152,7 +152,7 @@
152 152 </ul>
153 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 156 <h4><strong>{% trans "Order by" %}:</strong></h4>
157 157 <ul class="unstyled-list">
158 158 <li>
... ... @@ -165,8 +165,21 @@
165 165 </li>
166 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 183 <ul class="unstyled-list">
171 184 <li>
172 185 <span class="glyphicon glyphicon-chevron-right"></span>
... ... @@ -190,6 +203,16 @@
190 203 <h5>{{ total_votes }} {% trans "times" %}</h5>
191 204 </li>
192 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 216 </div>
194 217  
195 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 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 5 {% endfor %}
9   -{% endcache %}
... ...
src/super_archives/templatetags/superarchives.py
1 1  
2   -import re
3   -
4 2 from django import template
5   -from django.core.cache import cache
6   -
7   -from html2text import html2text
8 3  
9 4  
10 5 register = template.Library()
11 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 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   -
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 @@
  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 @@
  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 @@
  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 14 from django.shortcuts import render, redirect
15 15  
16 16 from . import queries
17   -from .utils import send_verification_email
18 17 from .decorators import count_hit
  18 +from .utils.email import send_verification_email
19 19 from .models import MailingList, Thread, EmailAddress, EmailAddressValidation
20 20  
21 21  
... ... @@ -42,18 +42,19 @@ def thread(request, mailinglist, thread_token):
42 42 total_votes += email.votes_count()
43 43  
44 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 47 thread.update_score()
48 48  
49   - template_data = {
  49 + context = {
50 50 'first_msg': first_message,
51 51 'emails': [first_message] + list(emails),
52 52 'pagehits': queries.get_page_hits(request.path_info),
53 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 60 def list_messages(request):
... ...