diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 61925837..2a8a0798 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -289,6 +289,13 @@ class IssueStatusAdmin(admin.ModelAdmin):
class ProjectTemplateAdmin(admin.ModelAdmin):
pass
+
+class GameAdmin(admin.ModelAdmin):
+ list_display = ["project", "name", "uuid"]
+ list_display_links = ["name"]
+ raw_id_fields = ["project"]
+
+
admin.site.register(models.IssueStatus, IssueStatusAdmin)
admin.site.register(models.TaskStatus, TaskStatusAdmin)
admin.site.register(models.UserStoryStatus, UserStoryStatusAdmin)
@@ -299,3 +306,4 @@ admin.site.register(models.Severity, SeverityAdmin)
admin.site.register(models.Priority, PriorityAdmin)
admin.site.register(models.IssueType, IssueTypeAdmin)
admin.site.register(models.ProjectTemplate, ProjectTemplateAdmin)
+admin.site.register(models.Game, GameAdmin)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 007e939c..40b63015 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -1070,3 +1070,11 @@ class InvitationViewSet(ModelListViewSet):
def list(self, *args, **kwargs):
raise exc.PermissionDenied(_("You don't have permisions to see that."))
+
+
+class GameViewSet(ModelCrudViewSet):
+ model = models.Game
+ serializer_class = serializers.GameSerializer
+ validator_class = validators.GameValidator
+ permission_classes = (permissions.GamePermission,)
+ filter_fields = ('project', 'uuid')
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 9db260df..f7894228 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import uuid
+
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
@@ -1261,3 +1263,31 @@ class ProjectTemplate(TaggedMixin, TagsColorsMixin, models.Model):
project.looking_for_people_note = self.looking_for_people_note
return project
+
+
+class Game(models.Model):
+ uuid = models.CharField(max_length=32, editable=False, null=True,
+ blank=True, default=None, db_index=True)
+ name = models.CharField(max_length=250, null=False, blank=False,
+ verbose_name=_("name"))
+ project = models.ForeignKey(Project, null=False, blank=False)
+ created_at = models.DateTimeField(default=timezone.now,
+ verbose_name=_("create at"))
+ end_at = models.DateTimeField(null=True, blank=True)
+ userstories = JSONField()
+ scales = JSONField()
+ roles = JSONField()
+
+ def save(self, *args, **kwargs):
+ if not self.uuid:
+ self.uuid = uuid.uuid4().hex
+ return super().save(*args, **kwargs)
+
+ class Meta:
+ verbose_name = "Game"
+ verbose_name_plural = "Games"
+ ordering = ["project", "name", "uuid"]
+ unique_together = ("project", "uuid")
+
+ def __str__(self):
+ return self.name
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index e7b15c9a..4c731475 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -241,3 +241,11 @@ class ProjectTemplatePermission(TaigaResourcePermission):
partial_update_perms = IsSuperUser()
destroy_perms = IsSuperUser()
list_perms = AllowAny()
+
+class GamePermission(TaigaResourcePermission):
+ retrieve_perms = HasProjectPerm('modify_us')
+ create_perms = HasProjectPerm('modify_us')
+ update_perms = HasProjectPerm('modify_us')
+ partial_update_perms = HasProjectPerm('modify_us')
+ destroy_perms = HasProjectPerm('modify_us')
+ list_perms = HasProjectPerm('modify_us')
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 4d138e71..2065585a 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -523,3 +523,16 @@ class ProjectTemplateSerializer(serializers.LightSerializer):
priorities = Field()
severities = Field()
roles = Field()
+
+
+class GameSerializer(serializers.LightSerializer):
+ uuid = Field()
+ name = Field()
+ project = Field(attr="project_id")
+ created_at = Field()
+ end_at = Field()
+ userstories = Field()
+ scales = Field()
+ roles = Field()
+
+
diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py
index b3abe1e6..d2d512fb 100644
--- a/taiga/projects/validators.py
+++ b/taiga/projects/validators.py
@@ -318,3 +318,60 @@ class DuplicateProjectValidator(validators.Validator):
description = serializers.CharField()
is_private = serializers.BooleanField()
users = DuplicateProjectMemberValidator(many=True)
+
+
+class GameValidator(validators.ModelValidator):
+ class Meta:
+ model = models.Game
+
+ def validate_roles(self, attrs, source):
+ project = attrs.get("project", None if self.object is None else self.object.project)
+ if project is None:
+ return attrs
+
+ roles = attrs[source]
+ if not isinstance(roles, list):
+ raise ValidationError(_("Invalid roles format"))
+
+ for role in roles:
+ if "id" not in role or "name" not in role:
+ raise ValidationError(_("Invalid role format"))
+
+ if project.roles.filter(id=role['id']).count() == 0:
+ raise ValidationError(_("Invalid role for the project"))
+
+ return attrs
+
+ def validate_scales(self, attrs, source):
+ scales = attrs[source]
+ if not isinstance(scales, list):
+ raise ValidationError(_("Invalid scales format"))
+
+ for scale in scales:
+ if "id" not in scale or "name" not in scale:
+ raise ValidationError(_("Invalid scale format"))
+
+ return attrs
+
+ def validate_userstories(self, attrs, source):
+ project = attrs.get("project", None if self.object is None else self.object.project)
+ if project is None:
+ return attrs
+
+ userstories = attrs[source]
+ if not isinstance(userstories, list):
+ raise ValidationError(_("Invalid user stories format"))
+
+ scales = map(lambda x: x['id'], attrs["scales"])
+
+ for us in userstories:
+ if "id" not in us or "scale_id" not in us:
+ raise ValidationError(_("Invalid user story format"))
+
+ if project.user_stories.filter(id=us['id']).count() == 0:
+ raise ValidationError(_("Invalid user story for the project"))
+
+ if us['scale_id'] is not None and us['scale_id'] not in scales:
+ raise ValidationError(_("Invalid scale id for user story"))
+
+ return attrs
diff --git a/taiga/routers.py b/taiga/routers.py
index e70203c9..a4d4a674 100644
--- a/taiga/routers.py
+++ b/taiga/routers.py
@@ -77,6 +77,7 @@ from taiga.projects.api import IssueDueDateViewSet
from taiga.projects.api import PriorityViewSet
from taiga.projects.api import SeverityViewSet
from taiga.projects.api import ProjectTemplateViewSet
+from taiga.projects.api import GameViewSet
router.register(r"projects", ProjectViewSet, base_name="projects")
router.register(r"projects/(?P\d+)/fans", ProjectFansViewSet, base_name="project-fans")
@@ -95,6 +96,7 @@ router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types")
router.register(r"issue-due-dates", IssueDueDateViewSet, base_name="issue-due-dates")
router.register(r"priorities", PriorityViewSet, base_name="priorities")
router.register(r"severities",SeverityViewSet , base_name="severities")
+router.register(r"games", GameViewSet, base_name="games")
# Custom Attributes