Просмотр исходного кода

Display attachments in posts footers. #29

Rafał Pitoń 11 лет назад
Родитель
Сommit
b208c46c1a

+ 15 - 2
misago/acl/permissions/threads.py

@@ -661,6 +661,19 @@ class ThreadsACL(BaseACL):
         if not self.can_upload_attachments(forum):
             raise ACLError403(_("You don't have permission to upload files in this forum."))
 
+    def can_download_attachments(self, user, forum, post):
+        try:
+            if user.is_authenticated() and user.id == post.user_id:
+                return True
+            forum_role = self.get_role(forum)
+            return forum_role['can_download_attachments']
+        except KeyError:
+            return False
+
+    def allow_attachment_download(self, user, forum, post):
+        if not self.can_download_attachments(user, forum, post):
+            raise ACLError403(_("You don't have permission to download this attachment."))
+
     def attachment_size_limit(self, forum):
         try:
             forum_role = self.get_role(forum)
@@ -677,7 +690,7 @@ class ThreadsACL(BaseACL):
 
     def can_delete_attachment(self, user, forum, attachment):
         if user.pk == attachment.pk:
-            return True
+            return None
         try:
             forum_role = self.get_role(forum)
             return forum_role['can_delete_attachments']
@@ -686,7 +699,7 @@ class ThreadsACL(BaseACL):
 
     def allow_attachment_delete(self, user, forum, attachment):
         if user.pk == attachment.pk:
-            return True
+            return None
         try:
             forum_role = self.get_role(forum)
             if not forum_role['can_delete_attachments']:

+ 1 - 1
misago/apps/attachments.py

@@ -7,7 +7,7 @@ from misago.shortcuts import render_to_response
 
 def server(self, attachment, thumb=False):
     try:
-        attachment = Attachment.objects.select_related('forum', 'thread', 'user').get(id=attachment)
+        attachment = Attachment.objects.select_related('forum', 'thread', 'user').get(hash_id=attachment)
         if thumb:
             response = StreamingHttpResponse(open(attachment.thumb_path), content_type=attachment.content_type)
         else:

+ 3 - 1
misago/apps/threadtype/posting/base.py

@@ -158,6 +158,7 @@ class PostingBaseView(ViewBase):
                                               uploaded_file.size)
 
             new_attachment = Attachment()
+            new_attachment.generate_hash_id(self.attachments_token)
             new_attachment.session = self.attachments_token
             new_attachment.filetype = attachment_type
             new_attachment.user = self.request.user
@@ -226,13 +227,14 @@ class PostingBaseView(ViewBase):
             if attachment.pk in self.attachments_removed:
                 attachment.delete()
             else:
+                post_attachments.append(attachment)
                 attachment.forum = self.forum
                 attachment.thread = self.thread
                 attachment.post = self.post
                 attachment.session = self.attachments_token
                 attachment.save()
 
-        if post_attachments:
+        if self.post.has_attachments or post_attachments:
             self.post.attachments = post_attachments
             self.post.save(force_update=True)
 

+ 479 - 0
misago/migrations/0034_auto__add_field_attachment_hash_id.py

@@ -0,0 +1,479 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Attachment.hash_id'
+        db.add_column(u'misago_attachment', 'hash_id',
+                      self.gf('django.db.models.fields.CharField')(default='-', max_length=8, db_index=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Attachment.hash_id'
+        db.delete_column(u'misago_attachment', 'hash_id')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'content_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'filetype': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.AttachmentType']"}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'hash_id': ('django.db.models.fields.CharField', [], {'max_length': '8', 'db_index': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'session': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'size': ('django.db.models.fields.PositiveIntegerField', [], {'max_length': '255'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_name_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.attachmenttype': {
+            'Meta': {'object_name': 'AttachmentType'},
+            'extensions': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'size_limit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'extra': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.poll': {
+            'Meta': {'object_name': 'Poll'},
+            '_choices_cache': ('django.db.models.fields.TextField', [], {'db_column': "'choices_cache'"}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'max_choices': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'poll_of'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'vote_changing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.polloption': {
+            'Meta': {'object_name': 'PollOption'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'option_set'", 'to': "orm['misago.Poll']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.pollvote': {
+            'Meta': {'object_name': 'PollVote'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.PollOption']", 'null': 'True', 'blank': 'True'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vote_set'", 'to': "orm['misago.Poll']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            '_attachments': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'attachments'", 'blank': 'True'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'delete_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'has_attachments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'reports': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'has_poll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'prefix': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.ThreadPrefix']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadprefix': {
+            'Meta': {'object_name': 'ThreadPrefix'},
+            'forums': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Forum']", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            '_avatar_crop': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'avatar_crop'", 'blank': 'True'}),
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_vote': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'starter': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.User']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 8 - 1
misago/models/attachmentmodel.py

@@ -16,6 +16,7 @@ class AttachmentManager(models.Manager):
 
 
 class Attachment(models.Model):
+    hash_id = models.CharField(max_length=8, db_index=True)
     filetype = models.ForeignKey('AttachmentType')
     forum = models.ForeignKey('Forum', null=True, blank=True, on_delete=models.SET_NULL)
     thread = models.ForeignKey('Thread', null=True, blank=True, on_delete=models.SET_NULL)
@@ -117,4 +118,10 @@ class Attachment(models.Model):
             image.thumbnail((800, 600), Image.ANTIALIAS)
             image.save(self.thumb_path)
         except IOError:
-            pass
+            pass
+
+    def generate_hash_id(self, seed):
+        unique_hash = seed
+        for i in xrange(100):
+            unique_hash = hashlib.sha256('%s:%s' % (settings.SECRET_KEY, unique_hash)).hexdigest()
+        self.hash_id = unique_hash[:8]

+ 0 - 3
misago/models/postmodel.py

@@ -89,9 +89,6 @@ class Post(models.Model):
         for attachment in new_attachments:
             attachment = copy.copy(attachment)
             attachment_user_pk = attachment.user_id
-            attachment_filetype_pk = attachment.filetype_id
-            attachment.filetype = None
-            attachment.filetype_id = attachment_filetype_pk
             attachment.user = None
             attachment.user_id = attachment_user_pk
             attachment.forum = None

+ 2 - 2
misago/urls.py

@@ -8,8 +8,8 @@ urlpatterns = patterns('misago.apps',
     url(r'^$', 'index.index', name="index"),
     url(r'^read-all/$', 'readall.read_all', name="read_all"),
     url(r'^register/$', 'register.views.form', name="register"),
-    url(r'^attachment/(?P<attachment>\d+)/$', 'attachments.server', name="attachments_server"),
-    url(r'^attachment/thumb/(?P<attachment>\d+)/$', 'attachments.server', name="attachments_thumbs_server", kwargs={'thumb': True}),
+    url(r'^attachment/(?P<attachment>[0-9a-zA-Z]{8})/$', 'attachments.server', name="attachments_server"),
+    url(r'^attachment/thumb/(?P<attachment>[0-9a-zA-Z]{8})/$', 'attachments.server', name="attachments_thumbs_server", kwargs={'thumb': True}),
     url(r'^category/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'category.category', name="category"),
     url(r'^redirect/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'redirect.redirect', name="redirect"),
     url(r'^alerts/$', 'alerts.alerts', name="alerts"),

+ 9 - 2
static/cranefly/css/cranefly.css

@@ -831,10 +831,9 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .editor .editor-upload{border-top:1px solid #e6e6e6;overflow:auto;padding:10.5px}
 .editor .editor-attachments{margin:0}.editor .editor-attachments li{border-top:1px solid #f0f0f0;overflow:auto;padding:10.5px}.editor .editor-attachments li .attachment-image{float:left}.editor .editor-attachments li .attachment-image i{display:block;height:40px;width:40px;color:#999;font-size:42px;text-align:center}
 .editor .editor-attachments li .attachment-image img{background-size:cover;border-radius:3px;height:40px;width:40px}
-.editor .editor-attachments li .attachment-body{margin-left:52px}.editor .editor-attachments li .attachment-body h4{margin:0;padding:0;font-size:14px}
+.editor .editor-attachments li .attachment-body{margin-left:52px}.editor .editor-attachments li .attachment-body h4{margin:0;padding:0;font-size:14px}.editor .editor-attachments li .attachment-body h4 .attachment-removed-message{color:#cf402e}
 .editor .editor-attachments li .attachment-body .attachment-details{color:#999;font-size:11.9px}.editor .editor-attachments li .attachment-body .attachment-details a:link,.editor .editor-attachments li .attachment-body .attachment-details a:visited{color:#555}
 .editor .editor-attachments li .attachment-body .attachment-details a:hover,.editor .editor-attachments li .attachment-body .attachment-details a:active{color:#222}
-.editor .editor-attachments li .attachment-body .attachment-details .attachment-removed-message{color:#cf402e}
 .editor .editor-attachments li .attachment-actions{float:right;margin-top:3px}.editor .editor-attachments li .attachment-actions .btn{margin-left:8px}.editor .editor-attachments li .attachment-actions .btn.btn-insert{color:#3bf}.editor .editor-attachments li .attachment-actions .btn.btn-insert:hover,.editor .editor-attachments li .attachment-actions .btn.btn-insert:active{color:#08c}
 .editor .editor-attachments li .attachment-actions .btn.btn-remove{color:#e38b80}.editor .editor-attachments li .attachment-actions .btn.btn-remove:hover,.editor .editor-attachments li .attachment-actions .btn.btn-remove:active{color:#cf402e}
 .editor .editor-attachments li.attachment-removed{opacity:.7;filter:alpha(opacity=70)}
@@ -1069,6 +1068,14 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .thread-body .post-wrapper .post-body .post-content .post-header .post-checkbox{float:right;position:relative;left:10px}
 .thread-body .post-wrapper .post-body .post-content .post-header .post-extra{float:right}.thread-body .post-wrapper .post-body .post-content .post-header .post-extra .label{margin-left:3.5px;text-shadow:none}.thread-body .post-wrapper .post-body .post-content .post-header .post-extra .label.label-purple{background-color:#7e2ecf}
 .thread-body .post-wrapper .post-body .post-content .post-message .markdown{padding:14px}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments{margin:0 14px}.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments h3{border-bottom:1px dotted #bfbfbf;margin:0;padding:0;color:#999;font-size:14px;line-height:20px}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments hr{background:none;border-top:1px dotted #d9d9d9 !important;margin:0;padding:0}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment{padding:10px 0}.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-image{float:left}.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-image i{display:block;height:40px;width:40px;color:#999;font-size:42px;text-align:center}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-image img{background-size:cover;border-radius:3px;height:40px;width:40px}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body{margin-left:52px}.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body h4{margin:0;padding:0;font-size:14px}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body .attachment-details{color:#999;font-size:11.9px}.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body .attachment-details a:link,.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body .attachment-details a:visited{color:#555}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body .attachment-details a:hover,.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body .attachment-details a:active{color:#222}
+.thread-body .post-wrapper .post-body .post-content .post-message .post-attachments .post-attachment .attachment-body .attachment-details .attachment-removed-message{color:#cf402e}
 .thread-body .post-wrapper .post-body .post-content .post-message .post-signature{border-top:1px dotted #bfbfbf;opacity:.6;filter:alpha(opacity=60);margin:0 14px;font-size:80%}.thread-body .post-wrapper .post-body .post-content .post-message .post-signature .markdown{padding:0;padding-top:5px;padding-bottom:14px}
 .thread-body .post-wrapper .post-body .post-content .post-footer{border-top:1px solid #e7e7e7;overflow:auto}.thread-body .post-wrapper .post-body .post-content .post-footer:empty{display:none}
 .thread-body .post-wrapper .post-body .post-content .post-footer .post-rating{float:left;overflow:auto;padding:7px 14px;border-right:1px dotted #e7e7e7}.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating a{color:#999}.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating a:hover,.thread-body .post-wrapper .post-body .post-content .post-footer .post-rating a a:active{color:#333}

+ 4 - 4
static/cranefly/css/cranefly/editor.less

@@ -108,6 +108,10 @@
           padding: 0px;
 
           font-size: @baseFontSize;
+
+          .attachment-removed-message {
+            color: @red;
+          }
         }
 
         .attachment-details {
@@ -121,10 +125,6 @@
           a:hover, a:active {
             color: @grayDarker;
           }
-
-          .attachment-removed-message {
-            color: @red;
-          }
         }
       }
 

+ 74 - 0
static/cranefly/css/cranefly/thread.less

@@ -234,6 +234,80 @@
             padding: @baseFontSize;
           }
 
+          .post-attachments {
+            margin: 0px @baseFontSize;
+
+            h3 {
+              border-bottom: 1px dotted darken(@postBackground, 25%);
+              margin: 0px;
+              padding: 0px;
+
+              color: @grayLight;
+              font-size: @baseFontSize;
+              line-height: @baseLineHeight;
+            }
+
+            hr {
+              background: none;
+              border-top: 1px dotted darken(@postBackground, 15%) !important;
+              margin: 0px;
+              padding: 0px;
+            }
+
+            .post-attachment {
+              padding: (@baseLineHeight / 2) 0px;
+
+              .attachment-image {
+                float: left;
+
+                i {
+                  display: block;
+                  height: 40px;
+                  width: 40px;
+
+                  color: @grayLight;
+                  font-size: 42px;
+                  text-align: center;
+                }
+
+                img {
+                  background-size: cover;
+                  border-radius: 3px;
+                  height: 40px;
+                  width: 40px;
+                }
+              }
+
+              .attachment-body {
+                margin-left: 52px;
+
+                h4 {
+                  margin: 0px;
+                  padding: 0px;
+
+                  font-size: @baseFontSize;
+                }
+
+                .attachment-details {
+                  color: @grayLight;
+                  font-size: @fontSizeSmall;
+
+                  a:link, a:visited {
+                    color: @gray;
+                  }
+
+                  a:hover, a:active {
+                    color: @grayDarker;
+                  }
+
+                  .attachment-removed-message {
+                    color: @red;
+                  }
+                }
+              }
+            }
+          }
+
           .post-signature {
             border-top: 1px dotted darken(@postBackground, 25%);
             .opacity(60);

+ 14 - 6
templates/cranefly/editor.html

@@ -68,9 +68,9 @@
   {% for attachment in attachments %}
   <li{% if attachment.pk in attachments_removed %} class="attachment-removed"{% endif %}>
     <div class="attachment-image">
-      <a href="{{ url('attachments_server', attachment=attachment.pk) }}">
+      <a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">
         {% if attachment.is_image %}
-        <img src="{{ url('attachments_thumbs_server', attachment=attachment.pk) }}" alt="">
+        <img src="{{ url('attachments_thumbs_server', attachment=attachment.hash_id) }}" alt="">
         {% else %}
         <i class="icon-file"></i>
         {% endif %}
@@ -80,17 +80,25 @@
       {% if attachment.pk in attachments_removed %}
       <button name="restore_attachment" value="{{ attachment.pk }}" type="submit" class="btn pull-right"><i class="icon-plus"></i> {% trans %}Restore{% endtrans %}</button>
       {% else %}
-      <button type="button" class="btn btn-insert pull-right editor-insert-attachment" data-attachment-md="{% if attachment.is_image %}[![{{ attachment.name }}]({{ url('attachments_thumbs_server', attachment=attachment.pk) }})]({{ url('attachments_server', attachment=attachment.pk) }}){% else %}[{{ attachment.name }}]({{ url('attachments_server', attachment=attachment.pk) }}){% endif %}"><i class="icon-share"></i> {% trans %}Insert{% endtrans %}</button>
+      <button type="button" class="btn btn-insert pull-right editor-insert-attachment" data-attachment-md="{% if attachment.is_image %}[![{{ attachment.name }}]({{ url('attachments_thumbs_server', attachment=attachment.hash_id) }})]({{ url('attachments_server', attachment=attachment.hash_id) }}){% else %}[{{ attachment.name }}]({{ url('attachments_server', attachment=attachment.hash_id) }}){% endif %}"><i class="icon-share"></i> {% trans %}Insert{% endtrans %}</button>
       {% if acl.threads.can_delete_attachment(user, forum, attachment) %}
       <button name="remove_attachment" value="{{ attachment.pk }}" type="submit" class="btn btn-remove pull-right"><i class="icon-remove"></i> {% trans %}Remove{% endtrans %}</button>
       {% endif %}
       {% endif %}
     </div>
     <div class="attachment-body">
-      <h4>{{ attachment.name }}</h4>
+      <h4>{% if attachment.pk in attachments_removed %}<span class="attachment-removed-message"><i class="icon-remove"></i> {% trans %}Removed:{% endtrans %}</span> {% endif %}{{ attachment.name }}</h4>
       <div class="attachment-details">
-        {% if attachment.pk in attachments_removed %}<strong class="attachment-removed-message">{% trans %}Removed{% endtrans %}</strong>{% endif %}
-        {% if attachment.user_id %}<a href="{{ url('user', user=attachment.user_id, username=attachment.user_name_slug) }}">{{ attachment.user_name }}</a>{% else %}{{ attachment.user_name }}{% endif %}, {{ attachment.date|date }}, {{ _(attachments_types[attachment.filetype_id].name) }}, {{ attachment.size|filesize }}
+        {% if attachment.user_id %}
+        <a href="{{ url('user', user=attachment.user_id, username=attachment.user_name_slug) }}">{{ attachment.user_name }}</a>,
+        {% else %}
+        {{ attachment.user_name }},
+        {% endif %}
+        {% if acl.users.can_see_users_trails() %}
+        <abbr title="{{ attachment.agent }}">{{ attachment.ip }}</abbr>,
+        {% endif %}
+        {{ attachment.date|date }},
+        {{ _(attachments_types[attachment.filetype_id].name) }}, {{ attachment.size|filesize }}
       </div>
     </div>
   </li>

+ 46 - 0
templates/cranefly/private_threads/thread.html

@@ -220,11 +220,56 @@
                 </div>
               </div>
               <div class="post-message">
+
                 <div class="markdown js-extra">
                   <article>
                     {{ post.post_preparsed|markdown_final|safe }}
                   </article>
                 </div>
+
+                {% if post.has_attachments and acl.threads.can_download_attachments(user, forum, post) %}
+                <div class="post-attachments">
+                  <h3>{% trans %}Attached files{% endtrans %}</h3>
+                  {% for row in post.attachments|batch(2) %}
+                  {% if not loop.first %}
+                  <hr>
+                  {% endif %}
+                  <div class="row-fluid">
+                    {% for attachment in row %}
+                    <div class="span6">
+                      <div class="post-attachment">
+                        <div class="attachment-image">
+                          <a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">
+                            {% if attachment.is_image %}
+                            <img src="{{ url('attachments_thumbs_server', attachment=attachment.hash_id) }}" alt="">
+                            {% else %}
+                            <i class="icon-file"></i>
+                            {% endif %}
+                          </a>
+                        </div>
+                        <div class="attachment-body">
+                          <h4><a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">{{ attachment.name }}</a></h4>
+                          <div class="attachment-details">
+                            {% if attachment.user_id %}
+                            <a href="{{ url('user', user=attachment.user_id, username=attachment.user_name_slug) }}">{{ attachment.user_name }}</a>,
+                            {% else %}
+                            {{ attachment.user_name }},
+                            {% endif %}
+                            {% if acl.users.can_see_users_trails() %}
+                            <abbr title="{{ attachment.agent }}">{{ attachment.ip }}</abbr>,
+                            {% endif %}
+                            {{ attachment.date|date }},
+                            {{ _(attachment.filetype.name) }}, {{ attachment.size|filesize }}
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                    {% endfor %}
+                  </div>
+                  {% endfor %}
+                </div>
+                {% endif %}
+
                 {% if post.user.signature %}
                 <div class="post-signature">
                   <div class="markdown">
@@ -232,6 +277,7 @@
                   </div>
                 </div>
                 {% endif %}
+
               </div>
               <div class="post-footer">{% filter trim %}
                 <div class="post-actions">

+ 45 - 0
templates/cranefly/reports/thread.html

@@ -172,11 +172,56 @@
             </div>
           </div>
           <div class="post-message">
+
             <div class="markdown js-extra">
               <article>
                 {{ post.post_preparsed|markdown_final|safe }}
               </article>
             </div>
+
+            {% if post.has_attachments and acl.threads.can_download_attachments(user, forum, post) %}
+            <div class="post-attachments">
+              <h3>{% trans %}Attached files{% endtrans %}</h3>
+              {% for row in post.attachments|batch(2) %}
+              {% if not loop.first %}
+              <hr>
+              {% endif %}
+              <div class="row-fluid">
+                {% for attachment in row %}
+                <div class="span6">
+                  <div class="post-attachment">
+                    <div class="attachment-image">
+                      <a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">
+                        {% if attachment.is_image %}
+                        <img src="{{ url('attachments_thumbs_server', attachment=attachment.hash_id) }}" alt="">
+                        {% else %}
+                        <i class="icon-file"></i>
+                        {% endif %}
+                      </a>
+                    </div>
+                    <div class="attachment-body">
+                      <h4><a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">{{ attachment.name }}</a></h4>
+                      <div class="attachment-details">
+                        {% if attachment.user_id %}
+                        <a href="{{ url('user', user=attachment.user_id, username=attachment.user_name_slug) }}">{{ attachment.user_name }}</a>,
+                        {% else %}
+                        {{ attachment.user_name }},
+                        {% endif %}
+                        {% if acl.users.can_see_users_trails() %}
+                        <abbr title="{{ attachment.agent }}">{{ attachment.ip }}</abbr>,
+                        {% endif %}
+                        {{ attachment.date|date }},
+                        {{ _(attachment.filetype.name) }}, {{ attachment.size|filesize }}
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                {% endfor %}
+              </div>
+              {% endfor %}
+            </div>
+            {% endif %}
+
           </div>
           <div class="post-footer">{% filter trim %}
             {% if post.pk == thread.start_post_id %}

+ 46 - 0
templates/cranefly/threads/thread.html

@@ -286,11 +286,56 @@
             </div>
           </div>
           <div class="post-message">
+
             <div class="markdown js-extra">
               <article>
                 {{ post.post_preparsed|markdown_final|safe }}
               </article>
             </div>
+
+            {% if post.has_attachments and acl.threads.can_download_attachments(user, forum, post) %}
+            <div class="post-attachments">
+              <h3>{% trans %}Attached files{% endtrans %}</h3>
+              {% for row in post.attachments|batch(2) %}
+              {% if not loop.first %}
+              <hr>
+              {% endif %}
+              <div class="row-fluid">
+                {% for attachment in row %}
+                <div class="span6">
+                  <div class="post-attachment">
+                    <div class="attachment-image">
+                      <a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">
+                        {% if attachment.is_image %}
+                        <img src="{{ url('attachments_thumbs_server', attachment=attachment.hash_id) }}" alt="">
+                        {% else %}
+                        <i class="icon-file"></i>
+                        {% endif %}
+                      </a>
+                    </div>
+                    <div class="attachment-body">
+                      <h4><a href="{{ url('attachments_server', attachment=attachment.hash_id) }}">{{ attachment.name }}</a></h4>
+                      <div class="attachment-details">
+                        {% if attachment.user_id %}
+                        <a href="{{ url('user', user=attachment.user_id, username=attachment.user_name_slug) }}">{{ attachment.user_name }}</a>,
+                        {% else %}
+                        {{ attachment.user_name }},
+                        {% endif %}
+                        {% if acl.users.can_see_users_trails() %}
+                        <abbr title="{{ attachment.agent }}">{{ attachment.ip }}</abbr>,
+                        {% endif %}
+                        {{ attachment.date|date }},
+                        {{ _(attachment.filetype.name) }}, {{ attachment.size|filesize }}
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                {% endfor %}
+              </div>
+              {% endfor %}
+            </div>
+            {% endif %}
+
             {% if post.user.signature %}
             <div class="post-signature">
               <div class="markdown">
@@ -298,6 +343,7 @@
               </div>
             </div>
             {% endif %}
+
           </div>
           <div class="post-footer">{% filter trim %}
             {% if acl.threads.can_see_post_score(forum) %}