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

rdmorganiser / rdmo / 14404330126

11 Apr 2025 01:31PM UTC coverage: 90.789% (+0.3%) from 90.478%
14404330126

push

github

web-flow
Merge pull request #1195 from rdmorganiser/2.3.0

RDMO 2.3.0 ⭐

989 of 1076 branches covered (91.91%)

9176 of 10107 relevant lines covered (90.79%)

3.63 hits per line

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

96.0
rdmo/projects/models/value.py
1
import mimetypes
4✔
2
from pathlib import Path
4✔
3

4
from django.db import models
4✔
5
from django.urls import reverse
4✔
6
from django.utils.translation import gettext_lazy as _
4✔
7

8
import iso8601
4✔
9
from django_cleanup import cleanup
4✔
10

11
from rdmo.core.constants import VALUE_TYPE_BOOLEAN, VALUE_TYPE_CHOICES, VALUE_TYPE_DATETIME, VALUE_TYPE_TEXT
4✔
12
from rdmo.core.models import Model
4✔
13
from rdmo.domain.models import Attribute
4✔
14
from rdmo.options.models import Option
4✔
15

16
from ..managers import ValueManager
4✔
17
from ..utils import get_value_path
4✔
18

19

20
def get_file_upload_to(instance, filename):
4✔
21
    return str(get_value_path(instance.project, instance.snapshot) / str(instance.id) / filename)
4✔
22

23

24
class Value(Model):
4✔
25

26
    objects = ValueManager()
4✔
27

28
    FALSE_TEXT = [None, '', '0', 'f', 'F', 'false', 'False']
4✔
29

30
    project = models.ForeignKey(
4✔
31
        'Project', on_delete=models.CASCADE, related_name='values',
32
        verbose_name=_('Project'),
33
        help_text=_('The project this value belongs to.')
34
    )
35
    snapshot = models.ForeignKey(
4✔
36
        'Snapshot', blank=True, null=True,
37
        on_delete=models.CASCADE, related_name='values',
38
        verbose_name=_('Snapshot'),
39
        help_text=_('The snapshot this value belongs to.')
40
    )
41
    attribute = models.ForeignKey(
4✔
42
        Attribute, blank=True, null=True,
43
        on_delete=models.SET_NULL, related_name='values',
44
        verbose_name=_('Attribute'),
45
        help_text=_('The attribute this value belongs to.')
46
    )
47
    set_prefix = models.CharField(
4✔
48
        max_length=16, blank=True, default='',
49
        verbose_name=_('Set prefix'),
50
        help_text=_('The position of this value with respect to superior sets (i.e. for nested question sets).')
51
    )
52
    set_index = models.IntegerField(
4✔
53
        default=0,
54
        verbose_name=_('Set index'),
55
        help_text=_('The position of this value in a set (i.e. for a question set tagged as collection).')
56
    )
57
    set_collection = models.BooleanField(
4✔
58
        null=True,
59
        verbose_name=_('Set collection'),
60
        help_text=_('Indicates if this value was entered as part of a set (important for conditions).')
61
    )
62
    collection_index = models.IntegerField(
4✔
63
        default=0,
64
        verbose_name=_('Collection index'),
65
        help_text=_('The position of this value in a list (i.e. for a question tagged as collection).')
66
    )
67
    text = models.TextField(
4✔
68
        blank=True,
69
        verbose_name=_('Text'),
70
        help_text=_('The string stored for this value.')
71
    )
72
    option = models.ForeignKey(
4✔
73
        Option, blank=True, null=True, on_delete=models.SET_NULL, related_name='values',
74
        verbose_name=_('Option'),
75
        help_text=_('The option stored for this value.')
76
    )
77
    file = models.FileField(
4✔
78
        upload_to=get_file_upload_to, null=True, blank=True,
79
        verbose_name=_('File'),
80
        help_text=_('The file stored for this value.')
81
    )
82
    value_type = models.CharField(
4✔
83
        max_length=8, choices=VALUE_TYPE_CHOICES, default=VALUE_TYPE_TEXT,
84
        verbose_name=_('Value type'),
85
        help_text=_('Type of this value.')
86
    )
87
    unit = models.CharField(
4✔
88
        max_length=64, blank=True,
89
        verbose_name=_('Unit'),
90
        help_text=_('Unit for this value.')
91
    )
92
    external_id = models.CharField(
4✔
93
        max_length=256, blank=True,
94
        verbose_name=_('External id'),
95
        help_text=_('External id for this value.')
96
    )
97

98
    class Meta:
4✔
99
        ordering = ('attribute', 'set_index', 'collection_index')
4✔
100
        verbose_name = _('Value')
4✔
101
        verbose_name_plural = _('Values')
4✔
102

103
    def __str__(self):
4✔
104
        return '{} / {} / {} / {} / {}'.format(
4✔
105
            self.project, self.snapshot or '-', self.set_prefix, self.set_index, self.collection_index
106
        )
107

108
    @property
4✔
109
    def as_dict(self):
4✔
110
        value_dict = {
4✔
111
            'id': self.id,
112
            'created': self.created,
113
            'updated': self.updated,
114
            'set_prefix': self.set_prefix,
115
            'set_index': self.set_index,
116
            'set_collection': self.set_collection,
117
            'collection_index': self.collection_index,
118
            'value_type': self.value_type,
119
            'unit': self.unit,
120
            'text': self.text,
121
            'option_uri': self.option_uri,
122
            'option_text': self.option_text,
123
            'option_additional_input': self.option_additional_input,
124
            'external_id': self.external_id,
125
            'value': self.value,
126
            'value_and_unit': self.value_and_unit,
127
            'is_true': self.is_true,
128
            'is_false': self.is_false,
129
            'is_empty': self.is_empty,
130
            'as_number': self.as_number
131
        }
132

133
        if self.file:
4✔
134
            value_dict.update({
4✔
135
                'file_name': self.file_name,
136
                'file_url': self.file_url,
137
                'file_type': self.file_type,
138
                'file_path': self.file_path
139
            })
140

141
        return value_dict
4✔
142

143
    @property
4✔
144
    def label(self):
4✔
145
        if self.option:
4✔
146
            return self.get_option_display(view=False)
4✔
147
        elif self.file:
4✔
148
            return self.get_file_display()
×
149
        elif self.text:
4✔
150
            return self.get_text_display()
4✔
151
        else:
152
            return ''
×
153

154
    @property
4✔
155
    def value(self):
4✔
156
        if self.option:
4✔
157
            return self.get_option_display(view=True)
4✔
158
        elif self.file:
4✔
159
            return self.get_file_display()
4✔
160
        elif self.text:
4✔
161
            return self.get_text_display()
4✔
162
        else:
163
            return ''
4✔
164

165
    @property
4✔
166
    def value_and_unit(self):
4✔
167
        if self.unit:
4✔
168
            return f'{self.value} {self.unit}'
×
169
        else:
170
            return self.value
4✔
171

172
    @property
4✔
173
    def is_true(self):
4✔
174
        return any([
4✔
175
            self.text not in self.FALSE_TEXT,
176
            self.option,
177
            self.file,
178
            self.external_id != ''
179
        ])
180

181
    @property
4✔
182
    def is_false(self):
4✔
183
        return all([
4✔
184
            self.text in self.FALSE_TEXT,
185
            not self.option,
186
            not self.file,
187
            self.external_id == ''
188
        ])
189

190
    @property
4✔
191
    def is_empty(self):
4✔
192
        return all([
4✔
193
            self.text == '',
194
            not self.option,
195
            not self.file,
196
            self.external_id == ''
197
        ])
198

199
    @property
4✔
200
    def as_number(self):
4✔
201
        try:
4✔
202
            val = self.text
4✔
203
        except AttributeError:
×
204
            return 0
×
205
        else:
206
            if isinstance(val, str):
4✔
207
                val = val.replace(',', '.')
4✔
208

209
            if isinstance(val, float) is False:
4✔
210
                try:
4✔
211
                    return int(val)
4✔
212
                except (ValueError, TypeError):
4✔
213
                    pass
4✔
214
                try:
4✔
215
                    return float(val)
4✔
216
                except (ValueError, TypeError):
4✔
217
                    return 0
4✔
218
            else:
219
                return val
×
220

221
    @property
4✔
222
    def file_name(self) -> str:
4✔
223
        if self.file:
4✔
224
            return Path(self.file.name).name
4✔
225

226
    @property
4✔
227
    def file_url(self) -> str:
4✔
228
        if self.file:
4✔
229
            return reverse('v1-projects:value-file', args=[self.id])
4✔
230

231
    @property
4✔
232
    def file_type(self) -> str:
4✔
233
        if self.file:
4✔
234
            return mimetypes.guess_type(self.file.name)[0]
4✔
235

236
    @property
4✔
237
    def file_path(self) -> Path:
4✔
238
        if self.file:
4✔
239
            resource_path = get_value_path(self.project, self.snapshot)
4✔
240
            return Path(self.file.name).relative_to(resource_path).as_posix()
4✔
241

242
    @property
4✔
243
    def attribute_uri(self) -> str:
4✔
244
        if self.attribute is not None:
4✔
245
            return self.attribute.uri
4✔
246

247
    @property
4✔
248
    def option_uri(self) -> str:
4✔
249
        if self.option is not None:
4✔
250
            return self.option.uri
4✔
251

252
    @property
4✔
253
    def option_text(self) -> str:
4✔
254
        if self.option is not None:
4✔
255
            return self.option.text
4✔
256

257
    @property
4✔
258
    def option_additional_input(self):
4✔
259
        if self.option is not None:
4✔
260
            return self.option.additional_input
4✔
261

262
    def copy_file(self, file_name, file_content):
4✔
263
        # copies a file field from a different value over to this value
264
        # this is tricky, because we need to trick django_cleanup to not delete the original file
265
        # important for snapshots and import from projects
266
        self.file.save(file_name, file_content, save=False)
4✔
267
        cleanup.refresh(self)
4✔
268
        self.save()
4✔
269

270
    def get_text_display(self):
4✔
271
        if self.value_type == VALUE_TYPE_DATETIME:
4✔
272
            try:
4✔
273
                return iso8601.parse_date(self.text).date()
4✔
274
            except iso8601.ParseError:
4✔
275
                return self.text
4✔
276
        elif self.value_type == VALUE_TYPE_BOOLEAN:
4✔
277
            if self.text == '1':
4✔
278
                return _('Yes')
4✔
279
            else:
280
                return _('No')
4✔
281
        else:
282
            return self.text
4✔
283

284
    def get_option_display(self, view=True):
4✔
285
        if view:
4✔
286
            string = self.option.view_text or self.option.text or ''
4✔
287
        else:
288
            string = self.option.text or ''
4✔
289
        if self.option.additional_input and self.text:
4✔
290
            string += ': ' + self.text
4✔
291
        return string
4✔
292

293
    def get_file_display(self):
4✔
294
        return self.file_name
4✔
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