• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rafalp / Misago / 17496798327

05 Sep 2025 02:59PM UTC coverage: 96.509% (-0.3%) from 96.769%
17496798327

Pull #1995

github

web-flow
Merge 17284d7d7 into 6bb938c90
Pull Request #1995: Move `Post` model from `misago.threads` to `misago.posts`

1884 of 1974 new or added lines in 149 files covered. (95.44%)

305 existing lines in 16 files now uncovered.

66716 of 69129 relevant lines covered (96.51%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.63
/misago/threads/models/attachment.py
1
import os
1✔
2
from hashlib import md5
1✔
3
from io import BytesIO
1✔
4

5
from django.core.files import File
1✔
6
from django.core.files.base import ContentFile
1✔
7
from django.db import models
1✔
8
from django.urls import reverse
1✔
9
from django.utils import timezone
1✔
10
from django.utils.crypto import get_random_string
1✔
11
from PIL import Image
1✔
12

13
from ...conf import settings
1✔
14
from ...core.utils import slugify
1✔
15
from ...plugins.models import PluginDataModel
1✔
16

17

18
def upload_to(instance, filename):
1✔
19
    # pylint: disable=undefined-loop-variable
UNCOV
20
    spread_path = md5(str(instance.secret[:16]).encode()).hexdigest()
×
UNCOV
21
    secret = Attachment.generate_new_secret()
×
22

UNCOV
23
    filename_lowered = filename.lower().strip()
×
UNCOV
24
    for extension in instance.filetype.extensions_list:
×
UNCOV
25
        if filename_lowered.endswith(extension):
×
UNCOV
26
            break
×
27

UNCOV
28
    filename_clean = ".".join(
×
29
        (slugify(filename[: (len(extension) + 1) * -1])[:16], extension)
30
    )
31

UNCOV
32
    return os.path.join(
×
33
        "attachments", spread_path[:2], spread_path[2:4], secret, filename_clean
34
    )
35

36

37
class Attachment(PluginDataModel):
1✔
38
    secret = models.CharField(max_length=64)
1✔
39
    filetype = models.ForeignKey("AttachmentType", on_delete=models.CASCADE)
1✔
40
    post = models.ForeignKey("Post", blank=True, null=True, on_delete=models.SET_NULL)
1✔
41

42
    uploaded_on = models.DateTimeField(default=timezone.now, db_index=True)
1✔
43

44
    uploader = models.ForeignKey(
1✔
45
        settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
46
    )
47
    uploader_name = models.CharField(max_length=255)
1✔
48
    uploader_slug = models.CharField(max_length=255, db_index=True)
1✔
49

50
    filename = models.CharField(max_length=255, db_index=True)
1✔
51
    size = models.PositiveIntegerField(default=0, db_index=True)
1✔
52

53
    thumbnail = models.ImageField(
1✔
54
        max_length=255, blank=True, null=True, upload_to=upload_to
55
    )
56
    image = models.ImageField(
1✔
57
        max_length=255, blank=True, null=True, upload_to=upload_to
58
    )
59
    file = models.FileField(max_length=255, blank=True, null=True, upload_to=upload_to)
1✔
60

61
    def __str__(self):
1✔
62
        return self.filename
×
63

64
    def delete(self, *args, **kwargs):
1✔
65
        self.delete_files()
×
66
        return super().delete(*args, **kwargs)
×
67

68
    def delete_files(self):
1✔
69
        if self.thumbnail:
×
70
            self.thumbnail.delete(save=False)
×
71
        if self.image:
×
72
            self.image.delete(save=False)
×
73
        if self.file:
×
74
            self.file.delete(save=False)
×
75

76
    @classmethod
1✔
77
    def generate_new_secret(cls):
1✔
UNCOV
78
        return get_random_string(settings.MISAGO_ATTACHMENT_SECRET_LENGTH)
×
79

80
    @property
1✔
81
    def is_image(self):
1✔
82
        return bool(self.image)
×
83

84
    @property
1✔
85
    def is_file(self):
1✔
86
        return not self.is_image
×
87

88
    def get_absolute_url(self):
1✔
89
        return reverse(
×
90
            "misago:attachment", kwargs={"pk": self.pk, "secret": self.secret}
91
        )
92

93
    def get_thumbnail_url(self):
1✔
94
        if self.thumbnail:
×
95
            return reverse(
×
96
                "misago:attachment-thumbnail",
97
                kwargs={"pk": self.pk, "secret": self.secret},
98
            )
99

100
    def set_file(self, upload):
1✔
101
        self.file = File(upload, upload.name)
×
102

103
    def set_image(self, upload):
1✔
104
        fileformat = self.filetype.extensions_list[0]
×
105

106
        self.image = File(upload, upload.name)
×
107

108
        thumbnail = Image.open(upload)
×
109
        downscale_image = (
×
110
            thumbnail.size[0] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[0]
111
            or thumbnail.size[1] > settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT[1]
112
        )
113
        strip_animation = fileformat == "gif"
×
114

115
        thumb_stream = BytesIO()
×
116
        if downscale_image:
×
117
            thumbnail.thumbnail(settings.MISAGO_ATTACHMENT_IMAGE_SIZE_LIMIT)
×
118
            if fileformat == "jpg":
×
119
                # normalize jpg to jpeg for Pillow
120
                thumbnail.save(thumb_stream, "jpeg")
×
121
            else:
122
                thumbnail.save(thumb_stream, "png")
×
123
        elif strip_animation:
×
124
            thumbnail.save(thumb_stream, "png")
×
125

126
        if downscale_image or strip_animation:
×
127
            self.thumbnail = ContentFile(thumb_stream.getvalue(), upload.name)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc