• 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

95.1
/misago/attachments/models.py
1
import os
1✔
2
from functools import cached_property
1✔
3
from hashlib import sha256
1✔
4
from typing import TYPE_CHECKING
1✔
5

6
from django.db import models
1✔
7
from django.urls import reverse
1✔
8
from django.utils import timezone
1✔
9
from django.utils.crypto import get_random_string
1✔
10

11
from ..conf import settings
1✔
12
from ..plugins.models import PluginDataModel
1✔
13
from .filetypes import AttachmentFileType, filetypes
1✔
14

15
if TYPE_CHECKING:
1✔
NEW
16
    from ..posts.models import Post
×
17

18

19
def upload_to(instance: "Attachment", filename: str) -> str:
1✔
20
    salt = sha256(get_random_string(32).encode()).hexdigest()
1✔
21
    filename, extension = instance.filetype.split_name(filename.lower())
1✔
22
    filename_clean = filename[:16] + "." + extension
1✔
23

24
    return os.path.join("attachments", salt[:2], salt[2:4], salt[-32:], filename_clean)
1✔
25

26

27
class Attachment(PluginDataModel):
1✔
28
    category = models.ForeignKey(
1✔
29
        "misago_categories.Category",
30
        blank=True,
31
        null=True,
32
        related_name="+",
33
        on_delete=models.SET_NULL,
34
    )
35
    thread = models.ForeignKey(
1✔
36
        "misago_threads.Thread",
37
        blank=True,
38
        null=True,
39
        related_name="+",
40
        on_delete=models.SET_NULL,
41
    )
42
    post = models.ForeignKey(
1✔
43
        "misago_posts.Post",
44
        blank=True,
45
        null=True,
46
        related_name="+",
47
        on_delete=models.SET_NULL,
48
    )
49

50
    uploader = models.ForeignKey(
1✔
51
        settings.AUTH_USER_MODEL,
52
        blank=True,
53
        null=True,
54
        related_name="+",
55
        on_delete=models.SET_NULL,
56
    )
57
    uploader_name = models.CharField(max_length=255)
1✔
58
    uploader_slug = models.CharField(max_length=255)
1✔
59

60
    uploaded_at = models.DateTimeField(default=timezone.now, db_index=True)
1✔
61

62
    name = models.CharField(max_length=255, db_index=True)
1✔
63
    slug = models.CharField(max_length=255)
1✔
64

65
    filetype_id = models.CharField(max_length=10, null=True)
1✔
66

67
    upload = models.FileField(max_length=255, upload_to=upload_to)
1✔
68
    dimensions = models.CharField(max_length=15, null=True)
1✔
69
    size = models.PositiveIntegerField(default=0)
1✔
70

71
    thumbnail = models.FileField(max_length=255, upload_to=upload_to)
1✔
72
    thumbnail_dimensions = models.CharField(max_length=15, null=True)
1✔
73
    thumbnail_size = models.PositiveIntegerField(default=0)
1✔
74

75
    is_deleted = models.BooleanField(default=False, db_index=True)
1✔
76

77
    upload_key: str  # or missing
1✔
78

79
    class Meta:
1✔
80
        indexes = [
1✔
81
            *PluginDataModel.Meta.indexes,
82
            models.Index(
83
                name="misago_attachment_miss_upload",
84
                fields=["upload"],
85
                condition=models.Q(upload=""),
86
            ),
87
        ]
88

89
    def __str__(self):
1✔
90
        return self.name
1✔
91

92
    def delete(self, *args, **kwargs):
1✔
93
        self.delete_files()
1✔
94
        return super().delete(*args, **kwargs)
1✔
95

96
    def delete_files(self):
1✔
97
        if self.upload:
1✔
98
            self.upload.delete(save=False)
1✔
99
        if self.thumbnail:
1✔
100
            self.thumbnail.delete(save=False)
1✔
101

102
    def associate_with_post(self, post: "Post"):
1✔
103
        self.category = post.category
1✔
104
        self.thread = post.thread
1✔
105
        self.post = post
1✔
106

107
    @cached_property
1✔
108
    def filetype(self) -> AttachmentFileType | None:
1✔
109
        if not self.filetype_id:
1✔
110
            raise ValueError(f"Attachment '{self.name}' is missing 'filetype_id'")
×
111

112
        try:
1✔
113
            return filetypes.get_filetype(self.filetype_id)
1✔
114
        except ValueError:
×
115
            return None
×
116

117
    @property
1✔
118
    def filetype_name(self) -> str:
1✔
119
        return str(self.filetype.name)
1✔
120

121
    @property
1✔
122
    def content_type(self) -> str:
1✔
123
        return self.filetype.content_types[0]
1✔
124

125
    @property
1✔
126
    def width(self) -> int | None:
1✔
127
        if self.dimensions:
1✔
128
            return int(self.dimensions.split("x")[0])
1✔
129

130
        return None
1✔
131

132
    @property
1✔
133
    def height(self) -> int | None:
1✔
134
        if self.dimensions:
1✔
135
            return int(self.dimensions.split("x")[1])
1✔
136

137
        return None
×
138

139
    @property
1✔
140
    def ratio(self) -> str | None:
1✔
141
        if self.dimensions:
1✔
142
            return (
1✔
143
                ("{:0.2f}".format(self.height * 100 / self.width))
144
                .rstrip("0")
145
                .rstrip(".")
146
            )
147

148
        return None
1✔
149

150
    @property
1✔
151
    def thumbnail_width(self) -> int | None:
1✔
152
        if self.thumbnail_dimensions:
1✔
153
            return int(self.thumbnail_dimensions.split("x")[0])
1✔
154

155
        return None
1✔
156

157
    @property
1✔
158
    def thumbnail_height(self) -> int | None:
1✔
159
        if self.thumbnail_dimensions:
1✔
160
            return int(self.thumbnail_dimensions.split("x")[1])
1✔
161

162
        return None
1✔
163

164
    def get_absolute_url(self) -> str:
1✔
165
        return reverse(
1✔
166
            "misago:attachment-download", kwargs={"id": self.id, "slug": self.slug}
167
        )
168

169
    def get_thumbnail_url(self) -> str | None:
1✔
170
        if not self.thumbnail:
1✔
171
            return None
1✔
172

173
        return reverse(
1✔
174
            "misago:attachment-thumbnail",
175
            kwargs={"id": self.id, "slug": self.slug},
176
        )
177

178
    def get_delete_url(self) -> str:
1✔
179
        return reverse(
1✔
180
            "misago:attachment-delete",
181
            kwargs={"id": self.id, "slug": self.slug},
182
        )
183

184
    def get_details_url(self) -> str:
1✔
185
        return reverse(
1✔
186
            "misago:attachment-details",
187
            kwargs={"id": self.id, "slug": self.slug},
188
        )
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