Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ jobs:
working-directory: backend
run: ruff check .

- name: Check formatting with Ruff
working-directory: backend
run: ruff format --check .

- name: Run tests
working-directory: backend
run: pytest apps/gameplay/
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Truth or Dare

[![CI](https://github.com/tigdav/truth-or-dare/actions/workflows/ci.yml/badge.svg)](https://github.com/tigdav/truth-or-dare/actions/workflows/ci.yml)
[![Ruff](https://img.shields.io/badge/code%20style-ruff-%23D7FF64?logo=ruff&logoColor=black&style=flat-square)](https://github.com/astral-sh/ruff)

![Python](https://img.shields.io/badge/Python-3.11.2-blue?logo=python&style=flat-square)
![Django](https://img.shields.io/badge/Django-5.2.15-%234092E5?logo=django&logoColor=white&style=flat-square)
Expand Down Expand Up @@ -167,6 +168,17 @@ The test suite covers core gameplay flows and endpoint behavior.

---

## Linting & Formatting

Code is linted and formatted with [Ruff](https://github.com/astral-sh/ruff):

```bash
ruff check .
ruff format --check .
```

---

## Project Structure

```
Expand Down
4 changes: 2 additions & 2 deletions backend/apps/gameplay/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


class GameplayConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.gameplay'
default_auto_field = "django.db.models.BigAutoField"
name = "apps.gameplay"
39 changes: 17 additions & 22 deletions backend/apps/gameplay/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,47 @@


class QuestionCategory(models.Model):
name = models.CharField(max_length=100, verbose_name='Category')
description = models.TextField(verbose_name='Short description')
is_adult = models.BooleanField(default=False, verbose_name='Adult category (18+)')
name = models.CharField(max_length=100, verbose_name="Category")
description = models.TextField(verbose_name="Short description")
is_adult = models.BooleanField(default=False, verbose_name="Adult category (18+)")

icon = models.FileField(upload_to='category_icons/', blank=True, null=True, verbose_name='Icon')
icon = models.FileField(upload_to="category_icons/", blank=True, null=True, verbose_name="Icon")

def __str__(self):
return self.name

class Meta:
verbose_name = 'Question Category'
verbose_name_plural = 'Question Categories'
verbose_name = "Question Category"
verbose_name_plural = "Question Categories"


class Question(models.Model):
text = models.TextField(verbose_name='Question text')
text = models.TextField(verbose_name="Question text")

question_type = models.CharField(
max_length=100,
choices=[('truth', 'Truth'), ('dare', 'Dare')],
verbose_name='Question type'
max_length=100, choices=[("truth", "Truth"), ("dare", "Dare")], verbose_name="Question type"
)

category = models.ForeignKey(
QuestionCategory,
on_delete=models.CASCADE,
related_name='questions',
verbose_name='Category'
QuestionCategory, on_delete=models.CASCADE, related_name="questions", verbose_name="Category"
)

def __str__(self):
return self.text

class Meta:
verbose_name = 'Question'
verbose_name_plural = 'Questions'
verbose_name = "Question"
verbose_name_plural = "Questions"


class Rule(models.Model):
text = models.TextField(verbose_name='Rule page content')
order = models.PositiveIntegerField(default=0, verbose_name='Display order')
text = models.TextField(verbose_name="Rule page content")
order = models.PositiveIntegerField(default=0, verbose_name="Display order")

class Meta:
verbose_name = 'Rule Page'
verbose_name_plural = 'Rule Pages'
ordering = ['order']
verbose_name = "Rule Page"
verbose_name_plural = "Rule Pages"
ordering = ["order"]

def __str__(self):
return f'Page {self.order + 1}'
return f"Page {self.order + 1}"
5 changes: 1 addition & 4 deletions backend/apps/gameplay/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,4 @@ class IsAdminOrReadOnly(BasePermission):
"""

def has_permission(self, request, view):
return (
request.method in SAFE_METHODS
or request.user and request.user.is_staff
)
return request.method in SAFE_METHODS or request.user and request.user.is_staff
20 changes: 7 additions & 13 deletions backend/apps/gameplay/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,26 @@
class QuestionCategorySerializer(serializers.ModelSerializer):
class Meta:
model = QuestionCategory
fields = ['id', 'name', 'description', 'icon', 'is_adult']
fields = ["id", "name", "description", "icon", "is_adult"]


class QuestionSerializer(serializers.ModelSerializer):
category_id = serializers.PrimaryKeyRelatedField(
source='category',
queryset=QuestionCategory.objects.all(),
required=True
source="category", queryset=QuestionCategory.objects.all(), required=True
)

class Meta:
model = Question
fields = ['id', 'text', 'question_type', 'category_id']
fields = ["id", "text", "question_type", "category_id"]


class QuestionRequestSerializer(serializers.Serializer):
question_type = serializers.ChoiceField(choices=['truth', 'dare'])
category_ids = serializers.ListField(
child=serializers.IntegerField(), allow_empty=False
)
excluded_ids = serializers.ListField(
child=serializers.IntegerField(), required=False
)
question_type = serializers.ChoiceField(choices=["truth", "dare"])
category_ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=False)
excluded_ids = serializers.ListField(child=serializers.IntegerField(), required=False)


class RuleSerializer(serializers.ModelSerializer):
class Meta:
model = Rule
fields = ['text']
fields = ["text"]
50 changes: 15 additions & 35 deletions backend/apps/gameplay/tests/test_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

@pytest.mark.django_db
def test_create_category():
User.objects.create_user(username='admin', password='adminpass', is_staff=True)
User.objects.create_user(username="admin", password="adminpass", is_staff=True)
client = APIClient()
client.login(username='admin', password='adminpass')
client.login(username="admin", password="adminpass")

data = {
"name": "Childhood & Memories",
"description": "Nostalgic stories, games, and funny moments from the past.",
"is_adult": False
"is_adult": False,
}

response = client.post("/api/categories/", data, format='json')
response = client.post("/api/categories/", data, format="json")

assert response.status_code == 201

Expand All @@ -36,11 +36,7 @@ def test_create_category():

@pytest.mark.django_db
def test_anonymous_can_list_categories():
QuestionCategory.objects.create(
name="Public Category",
description="Listed for anonymous clients.",
is_adult=False
)
QuestionCategory.objects.create(name="Public Category", description="Listed for anonymous clients.", is_adult=False)
client = APIClient()

response = client.get("/api/categories/")
Expand All @@ -51,11 +47,7 @@ def test_anonymous_can_list_categories():

@pytest.mark.django_db
def test_anonymous_can_retrieve_category():
category = QuestionCategory.objects.create(
name="Retrievable",
description="Detail view is public.",
is_adult=False
)
category = QuestionCategory.objects.create(name="Retrievable", description="Detail view is public.", is_adult=False)
client = APIClient()

response = client.get(f"/api/categories/{category.id}/")
Expand All @@ -66,39 +58,27 @@ def test_anonymous_can_retrieve_category():

@pytest.mark.django_db
def test_non_admin_cannot_create_category():
User.objects.create_user(username='alice', password='alicepass')
User.objects.create_user(username="alice", password="alicepass")
client = APIClient()
client.login(username='alice', password='alicepass')
client.login(username="alice", password="alicepass")

data = {
"name": "Forbidden",
"description": "Should not be created by a non-admin user.",
"is_adult": False
}
response = client.post("/api/categories/", data, format='json')
data = {"name": "Forbidden", "description": "Should not be created by a non-admin user.", "is_adult": False}
response = client.post("/api/categories/", data, format="json")

assert response.status_code == 403
assert QuestionCategory.objects.count() == 0


@pytest.mark.django_db
def test_admin_can_update_category():
category = QuestionCategory.objects.create(
name="Original",
description="Before update.",
is_adult=False
)
category = QuestionCategory.objects.create(name="Original", description="Before update.", is_adult=False)

User.objects.create_user(username='admin', password='adminpass', is_staff=True)
User.objects.create_user(username="admin", password="adminpass", is_staff=True)
client = APIClient()
client.login(username='admin', password='adminpass')
client.login(username="admin", password="adminpass")

data = {
"name": "Updated",
"description": "After update.",
"is_adult": True
}
response = client.put(f"/api/categories/{category.id}/", data, format='json')
data = {"name": "Updated", "description": "After update.", "is_adult": True}
response = client.put(f"/api/categories/{category.id}/", data, format="json")

assert response.status_code == 200

Expand Down
28 changes: 11 additions & 17 deletions backend/apps/gameplay/tests/test_questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ def test_create_dare_question_in_strange_habits_category():
id=6,
name="Unusual Habits",
description="Quirky rituals, everyday oddities, and personal peculiarities.",
is_adult=False
is_adult=False,
)

User.objects.create_user(username='admin', password='adminpass', is_staff=True)
User.objects.create_user(username="admin", password="adminpass", is_staff=True)

client = APIClient()
client.login(username='admin', password='adminpass')
client.login(username="admin", password="adminpass")

data = {
"text": "Do 10 squats while repeating your favorite weird word.",
"question_type": "dare",
"category_id": category.id
"category_id": category.id,
}

response = client.post("/api/questions/", data, format='json')
response = client.post("/api/questions/", data, format="json")

assert response.status_code == 201

Expand All @@ -51,9 +51,9 @@ def test_anonymous_cannot_list_questions():

@pytest.mark.django_db
def test_non_admin_cannot_list_questions():
User.objects.create_user(username='alice', password='alicepass')
User.objects.create_user(username="alice", password="alicepass")
client = APIClient()
client.login(username='alice', password='alicepass')
client.login(username="alice", password="alicepass")

response = client.get("/api/questions/")

Expand All @@ -63,19 +63,13 @@ def test_non_admin_cannot_list_questions():
@pytest.mark.django_db
def test_admin_can_list_questions():
category = QuestionCategory.objects.create(
name="Listable",
description="Questions can be listed for admins.",
is_adult=False
)
Question.objects.create(
text="Sample listed question?",
question_type="truth",
category=category
name="Listable", description="Questions can be listed for admins.", is_adult=False
)
Question.objects.create(text="Sample listed question?", question_type="truth", category=category)

User.objects.create_user(username='admin', password='adminpass', is_staff=True)
User.objects.create_user(username="admin", password="adminpass", is_staff=True)
client = APIClient()
client.login(username='admin', password='adminpass')
client.login(username="admin", password="adminpass")

response = client.get("/api/questions/")

Expand Down
Loading
Loading