|
15 | 15 | from django.core.validators import validate_unicode_slug |
16 | 16 | from django.core.validators import MaxValueValidator |
17 | 17 | from django.db import models |
| 18 | +from django.db.models.constraints import UniqueConstraint |
18 | 19 | from django.urls import reverse |
19 | 20 | from django.utils.functional import cached_property |
20 | 21 | from django.utils import timezone as tz_utils |
21 | 22 |
|
22 | | -from patchwork.fields import HashField |
| 23 | +from patchwork.fields import HashField, ColorField |
23 | 24 | from patchwork.hasher import hash_diff |
24 | 25 |
|
25 | 26 | if settings.ENABLE_REST_API: |
@@ -261,6 +262,60 @@ class Meta: |
261 | 262 | ordering = ['ordering'] |
262 | 263 |
|
263 | 264 |
|
| 265 | +class Label(models.Model): |
| 266 | + """Labels for patches. |
| 267 | +
|
| 268 | + Labels are arbitrary bits of metadata attached to a patch. They can |
| 269 | + be used to signify priority, category, or other similar information. |
| 270 | + They can also be used to filter patches and identify the ones most |
| 271 | + interesting to a given user. |
| 272 | + """ |
| 273 | + |
| 274 | + project = models.ForeignKey( |
| 275 | + Project, |
| 276 | + related_name='labels', |
| 277 | + related_query_name='label', |
| 278 | + null=True, |
| 279 | + blank=True, |
| 280 | + on_delete=models.CASCADE, |
| 281 | + help_text='The project this label is associated with. If unset, this ' |
| 282 | + 'label is global to the instance', |
| 283 | + ) |
| 284 | + |
| 285 | + name = models.CharField(max_length=25, help_text='The label value.') |
| 286 | + description = models.TextField( |
| 287 | + null=True, |
| 288 | + blank=True, |
| 289 | + help_text='A description of what the label is and when it should be ' |
| 290 | + 'applied.', |
| 291 | + ) |
| 292 | + color = ColorField(help_text='The color code to use in the UI.') |
| 293 | + |
| 294 | + def __str__(self): |
| 295 | + return self.name |
| 296 | + |
| 297 | + @staticmethod |
| 298 | + def get_project_labels(project): |
| 299 | + result = {} |
| 300 | + project_labels = Label.objects.filter( |
| 301 | + models.Q(project=project) | models.Q(project__isnull=True) |
| 302 | + ).all() |
| 303 | + |
| 304 | + for label in project_labels: |
| 305 | + if label.name not in result or label.project is not None: |
| 306 | + result[label.name] = label |
| 307 | + |
| 308 | + return list(result.values()) |
| 309 | + |
| 310 | + class Meta: |
| 311 | + ordering = ['name'] |
| 312 | + constraints = [ |
| 313 | + UniqueConstraint( |
| 314 | + 'project', Lower('name'), name='uniq_labels_project_id_name' |
| 315 | + ) |
| 316 | + ] |
| 317 | + |
| 318 | + |
264 | 319 | class Tag(models.Model): |
265 | 320 | name = models.CharField(max_length=20) |
266 | 321 | pattern = models.CharField( |
@@ -441,6 +496,7 @@ class SubmissionMixin(FilenameMixin, EmailMixin, models.Model): |
441 | 496 | # submission metadata |
442 | 497 |
|
443 | 498 | name = models.CharField(max_length=255) |
| 499 | + labels = models.ManyToManyField(Label) |
444 | 500 |
|
445 | 501 | @cached_property |
446 | 502 | def list_archive_url(self): |
|
0 commit comments