Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8ef2672
chore: configure pytest, github actions, and sonarcloud
PedroVFSantos Jul 14, 2026
e2d6b21
feat: add podcast category and is_published method with unit tests
PedroVFSantos Jul 14, 2026
073681b
chore: use psycopg2-binary instead of psycopg2
PedroVFSantos Jul 14, 2026
c676764
chore: add SECRET_KEY env to test step in CI
PedroVFSantos Jul 14, 2026
66ecf4f
chore: upgrade whitenoise to fix django.utils.six import error
PedroVFSantos Jul 14, 2026
f1669be
Refactor checkEmpty function for brevity
EduardoEudoro Jul 14, 2026
c11410c
Deactivate Django DEBUG mode in production (python:S4507)
Henpique Jul 14, 2026
f47acdf
docs: update README installation and test instructions, add .env to repo
PedroVFSantos Jul 14, 2026
43650ac
Merge remote-tracking branch 'origin/EduardoEudoro-patch-1'
PedroVFSantos Jul 14, 2026
f3b187e
Merge remote-tracking branch 'origin/HenriquePique'
PedroVFSantos Jul 14, 2026
00680a0
fix(S3752): add @require_GET to all function-based views to explicitl…
joaovmauad Jul 14, 2026
964a16c
feat: implement Tag.published_tutorials_count (M2) and fix settings.p…
Ulysses-Carneiro-Ufscar Jul 14, 2026
a5e0aae
refactor: replace unused local variable 'obj' with '_' in api/views.py
PedroVFSantos Jul 14, 2026
3343034
fix(settings): fix PROJECT_ROOT path and eliminate staticfiles warning
Ulysses-Carneiro-Ufscar Jul 14, 2026
8087ec4
chore: add localhost to ALLOWED_HOSTS
Ulysses-Carneiro-Ufscar Jul 14, 2026
9829889
Merge remote-tracking branch 'origin/feature/concluir-projeto-e-issue'
PedroVFSantos Jul 14, 2026
313e4aa
test: add views and api views unit tests to increase coverage to 77%
Ulysses-Carneiro-Ufscar Jul 14, 2026
023f681
Merge pull request #1 from PedroVFSantos/feature/concluir-projeto-e-i…
PedroVFSantos Jul 14, 2026
8ba6a8c
fix(ci): pin action commit SHAs, resolve SonarCloud S7637 and S5144, …
Ulysses-Carneiro-Ufscar Jul 14, 2026
940e3bf
test: add concise docstrings/comments to all new group tests
Ulysses-Carneiro-Ufscar Jul 14, 2026
bd68aa4
Merge remaining commits containing test comments and SonarCloud fixes
Ulysses-Carneiro-Ufscar Jul 14, 2026
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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SECRET_KEY=django-insecure-local-dev-key-es2-project
6 changes: 6 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Configure labels based on changed files
backend:
- '**.py'
frontend:
- 'app/templates/**'
- 'index.html'
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI/CD Pipeline

on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]

jobs:
test-and-sonar:
name: Test and SonarCloud Scan
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run Tests and generate Coverage
env:
SECRET_KEY: temporary-secret-key-for-testing
run: |
pytest --cov=. --cov-report=xml

- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/labeler@v2
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ celerybeat-schedule
*.sage.py

# Environments
.env
.venv
env/
venv/
Expand Down
69 changes: 29 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,59 +22,48 @@
- All the content (tutorials) is owned by the respective authors/sites.
- tutorialdb maintains its own database saving the links to tutorials and some meta info.

### Installation 🔮
### Installation & Execution 🔮

1. Create virtual environment.
1. Create a virtual environment:

**Linux/MacOS**
```bash
virtualenv -p python3 venv && cd venv && source bin/activate
python3 -m venv venv && source venv/bin/activate
```
**Windows**
(*PowerShell*)
```cmd
py -m venv venv; .\venv\Scripts\activate;
**Windows (PowerShell)**
```powershell
python -m venv venv
.\venv\Scripts\activate
```
*(Note: If you get execution policy errors on Windows, run `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process` first).*

2. Clone the repository.
2. Clone the repository and navigate to it.

```bash
git clone https://github.com/Bhupesh-V/tutorialdb.git
```

3. Install dependencies.

```bash
pip install -r requirements.txt
```

4. Set-up virtual environment variables.
1. Create a file named `.env` in the root directory & add the following contents.

```text
SECRET_KEY = 'my-secret-key'
LOCAL_HOST = 'my-local-ip'
3. Install dependencies:
```bash
pip install -r requirements.txt
```
2. For `SECRET_KEY` use [Django Secret Key Generator](https://www.miniwebtool.com/django-secret-key-generator/) or [Djecrety](https://djecrety.ir/).
3. Adding `LOCAL_HOST` is optional.

5. Migrate tables.

```bash
python manage.py migrate
```
*(Note: We updated the dependencies so that `psycopg2-binary` is used on Windows instead of the source version, preventing compilation issues).*

6. Run Tests.
4. Set up Environment Variables:
- The repository already comes with a default `.env` file containing a `SECRET_KEY` for development, so no manual configuration is required to start!

```bash
python manage.py test
```
5. Run Migrations:
```bash
python manage.py migrate
```

7. Run the development server.
6. Run Tests and Coverage:
To run the test suite and output the coverage report directly on the terminal, run:
```bash
pytest --cov=. --cov-report=term-missing
```

```bash
python manage.py runserver
```
7. Run the Development Server:
```bash
python manage.py runserver
```
Open your browser at `http://127.0.0.1:8000/`.

## 📝 License

Expand Down
40 changes: 34 additions & 6 deletions api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,41 @@
class APITests(TransactionTestCase):

def test_tutorials_page_status_code(self):
response = self.client.get('/tutorials/')
self.assertEquals(response.status_code, 200)
response = self.client.get('/api/tutorials/')
self.assertEqual(response.status_code, 200)

def test_tags_page_status_code(self):
response = self.client.get('/tags/')
self.assertEquals(response.status_code, 200)
response = self.client.get('/api/tags/')
self.assertEqual(response.status_code, 200)

def test_latest_page_status_code(self):
response = self.client.get('/latest/')
self.assertEquals(response.status_code, 200)
response = self.client.get('/api/latest/')
self.assertEqual(response.status_code, 200)

def test_tutorial_tag_api(self):
"""Valida o endpoint da API REST para filtragem de tutoriais por tag específica."""
from app.models import Tag, Tutorial
tag = Tag.objects.create(name="python")
t = Tutorial.objects.create(
title="Python Tutorial",
link="https://python.org",
category=Tutorial.DOCS,
publish=True
)
t.tags.add(tag)
response = self.client.get('/api/tutorials/python/')
self.assertEqual(response.status_code, 200)

def test_tutorial_tag_category_api(self):
"""Valida o endpoint da API REST para filtragem combinada de tag e categoria."""
from app.models import Tag, Tutorial
tag = Tag.objects.create(name="django")
t = Tutorial.objects.create(
title="Django Tutorial",
link="https://djangoproject.com",
category=Tutorial.DOCS,
publish=True
)
t.tags.add(tag)
response = self.client.get('/api/tutorials/django/docs/')
self.assertEqual(response.status_code, 200)
2 changes: 1 addition & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def tutorials(request):
category=request.data['category']
)
for tag in tags:
obj, created = Tag.objects.get_or_create(name=tag)
_, created = Tag.objects.get_or_create(name=tag)

tag_obj_list = Tag.objects.filter(name__in=tags)
tutorial_object.tags.set(tag_obj_list)
Expand Down
10 changes: 10 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class Tag(models.Model):
def __str__(self):
return self.name

def published_tutorials_count(self):
"""Returns the number of published tutorials associated with this tag"""
return self.tutorial_set.filter(publish=True).count()


class Tutorial(models.Model):
"""tutorials have a title, a URL, a set of tags, a category and creation date"""
Expand All @@ -20,6 +24,7 @@ class Tutorial(models.Model):
COURSE = 'course'
DOCS = 'docs'
VIDEO = 'video'
PODCAST = 'podcast'

CATEGORIES = (
(ARTICLE, 'Article'),
Expand All @@ -28,6 +33,7 @@ class Tutorial(models.Model):
(COURSE, 'Course'),
(DOCS, 'Documentation'),
(VIDEO, 'Video'),
(PODCAST, 'Podcast'),
)

title = models.CharField(max_length=200)
Expand All @@ -39,3 +45,7 @@ class Tutorial(models.Model):

def __str__(self):
return self.title

def is_published(self):
"""Returns True if the tutorial is published"""
return self.publish
8 changes: 3 additions & 5 deletions app/static/app/js/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ function share(title, link) {
}

function checkEmpty() {
var input = document.getElementById("search-bar");
if (input.value === "" || input.value === null) {
return false;
}
return true;
var input = document.getElementById("search-bar");

return input.value !== "" && input.value !== null;
}
2 changes: 1 addition & 1 deletion app/templates/about.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'base.html'%}
{% load staticfiles %}
{% load static %}
{% block content %}
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
Expand Down
2 changes: 1 addition & 1 deletion app/templates/base.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load staticfiles %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/contribute.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load static %}
{% block content %}
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
Expand Down
2 changes: 1 addition & 1 deletion app/templates/home.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load static %}
{% block content %}
<section class="hero is-medium is-dark is-bold">
<div class="hero-body" style="background-color: #04243B">
Expand Down
2 changes: 1 addition & 1 deletion app/templates/latest.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'base.html'%}
{% load staticfiles %}
{% load static %}
{% block content %}
{% if tutorials %}
<div class="section">
Expand Down
2 changes: 1 addition & 1 deletion app/templates/search_results.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'home.html' %}
{% load staticfiles %}
{% load static %}
{% block results %}
<div id="focus-here">
{% if tutorials %}
Expand Down
2 changes: 1 addition & 1 deletion app/templates/taglinks.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block content %}
{% load staticfiles %}
{% load static %}
{% if tutorials %}
<div class="section">
<h2 class="title is-4">Tutorials Tagged
Expand Down
2 changes: 1 addition & 1 deletion app/templates/tags.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% load static %}
{% block content %}
<div class="section">
<div class="container">
Expand Down
2 changes: 1 addition & 1 deletion app/templates/thankyou.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'base.html'%}
{% load staticfiles %}
{% load static %}
{% block content %}
<center>
<section class="hero is-medium is-primary is-bold">
Expand Down
59 changes: 59 additions & 0 deletions app/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.test import TestCase
from app.models import Tag, Tutorial

class TagModelTest(TestCase):
def test_string_representation(self):
"""Valida que a representação em string da Tag é o seu próprio nome."""
tag = Tag(name="Python")
self.assertEqual(str(tag), tag.name)

def test_published_tutorials_count(self):
"""Valida a contagem exclusiva de tutoriais publicados vinculados a uma tag (M2)."""
tag = Tag.objects.create(name="Django")

t1 = Tutorial.objects.create(
title="Django Tutorial 1",
link="https://example.com/1",
category=Tutorial.DOCS,
publish=True
)
t1.tags.add(tag)

t2 = Tutorial.objects.create(
title="Django Tutorial 2",
link="https://example.com/2",
category=Tutorial.DOCS,
publish=False
)
t2.tags.add(tag)

self.assertEqual(tag.published_tutorials_count(), 1)

class TutorialModelTest(TestCase):
def setUp(self):
self.tutorial = Tutorial(
title="Learn Django",
link="https://docs.djangoproject.com/",
category=Tutorial.DOCS,
publish=True
)
self.tutorial.save()

def test_string_representation(self):
"""Valida que a representação em string do Tutorial é o seu próprio título."""
self.assertEqual(str(self.tutorial), self.tutorial.title)

def test_is_published(self):
"""Valida que is_published retorna True para tutoriais publicados (M1)."""
self.assertTrue(self.tutorial.is_published())

def test_not_published(self):
"""Valida que is_published retorna False para tutoriais rascunho (M1)."""
draft = Tutorial(
title="Draft",
link="http://example.com",
category=Tutorial.ARTICLE,
publish=False
)
draft.save()
self.assertFalse(draft.is_published())
Loading