From 8ef2672d22d237e3682209ed514960b09e8f83ee Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Mon, 13 Jul 2026 23:28:58 -0300 Subject: [PATCH 01/16] chore: configure pytest, github actions, and sonarcloud --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ pytest.ini | 3 +++ requirements.txt | 5 ++++- sonar-project.properties | 6 ++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 pytest.ini create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1344a3d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +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 + run: | + pytest --cov=. --cov-report=xml + + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c0c502f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE = tutorialdb.settings +python_files = tests.py test_*.py *_tests.py diff --git a/requirements.txt b/requirements.txt index 8534398..1cb775c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,7 @@ django-import-export>=1.0.0 whitenoise==3.3.1 dj-database-url==0.4.2 gunicorn==19.7.1 -psycopg2==2.7.3.2 \ No newline at end of file +psycopg2==2.7.3.2 +pytest>=7.0.0 +pytest-django>=4.5.2 +pytest-cov>=4.0.0 \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..3b2600d --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,6 @@ +sonar.projectKey=PedroVFSantos_tutorialdb_ES2 +sonar.organization=pedrovfsantos +sonar.python.coverage.reportPaths=coverage.xml +sonar.sources=app +sonar.tests=app/tests +sonar.language=py From e2d6b210296685a3055596a1f71a3d33c8fe3965 Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Mon, 13 Jul 2026 23:29:27 -0300 Subject: [PATCH 02/16] feat: add podcast category and is_published method with unit tests --- app/models.py | 6 ++++++ app/tests/test_models.py | 33 +++++++++++++++++++++++++++++++++ app/tests/test_views.py | 12 ++++++------ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 app/tests/test_models.py diff --git a/app/models.py b/app/models.py index 82c38fc..5cfb9b7 100644 --- a/app/models.py +++ b/app/models.py @@ -20,6 +20,7 @@ class Tutorial(models.Model): COURSE = 'course' DOCS = 'docs' VIDEO = 'video' + PODCAST = 'podcast' CATEGORIES = ( (ARTICLE, 'Article'), @@ -28,6 +29,7 @@ class Tutorial(models.Model): (COURSE, 'Course'), (DOCS, 'Documentation'), (VIDEO, 'Video'), + (PODCAST, 'Podcast'), ) title = models.CharField(max_length=200) @@ -39,3 +41,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 diff --git a/app/tests/test_models.py b/app/tests/test_models.py new file mode 100644 index 0000000..1c10f0c --- /dev/null +++ b/app/tests/test_models.py @@ -0,0 +1,33 @@ +from django.test import TestCase +from app.models import Tag, Tutorial + +class TagModelTest(TestCase): + def test_string_representation(self): + tag = Tag(name="Python") + self.assertEqual(str(tag), tag.name) + +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): + self.assertEqual(str(self.tutorial), self.tutorial.title) + + def test_is_published(self): + self.assertTrue(self.tutorial.is_published()) + + def test_not_published(self): + draft = Tutorial( + title="Draft", + link="http://example.com", + category=Tutorial.ARTICLE, + publish=False + ) + draft.save() + self.assertFalse(draft.is_published()) diff --git a/app/tests/test_views.py b/app/tests/test_views.py index 1419a98..7a61601 100644 --- a/app/tests/test_views.py +++ b/app/tests/test_views.py @@ -5,30 +5,30 @@ class StaticPageTests(SimpleTestCase): def test_home_page_status_code(self): response = self.client.get('/') - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_api_page_status_code(self): response = self.client.get('/api/') - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_about_page_status_code(self): response = self.client.get('/about/') - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_contribute_page_status_code(self): response = self.client.get('/contribute/') - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) class DynamicPageTests(TransactionTestCase): def test_latest_page_status_code(self): response = self.client.get('/latest/') - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_tags_page_status_code(self): response = self.client.get('/tags/') - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) class TestTemplateNames(TransactionTestCase): From 073681bf904b96d1efb028e839fc7989ea31eb9e Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Mon, 13 Jul 2026 23:58:33 -0300 Subject: [PATCH 03/16] chore: use psycopg2-binary instead of psycopg2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1cb775c..cc0ed79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-import-export>=1.0.0 whitenoise==3.3.1 dj-database-url==0.4.2 gunicorn==19.7.1 -psycopg2==2.7.3.2 +psycopg2-binary>=2.7.3.2 pytest>=7.0.0 pytest-django>=4.5.2 pytest-cov>=4.0.0 \ No newline at end of file From c676764eb48ee293016a697d35c900d0a05fa807 Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Tue, 14 Jul 2026 00:03:57 -0300 Subject: [PATCH 04/16] chore: add SECRET_KEY env to test step in CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1344a3d..b5f7fe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: 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 From 66ecf4ff71d4093c010ecdf034fe97d4b1a3d719 Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Tue, 14 Jul 2026 00:10:04 -0300 Subject: [PATCH 05/16] chore: upgrade whitenoise to fix django.utils.six import error --- requirements.txt | 2 +- tutorialdb/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index cc0ed79..6cdea03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ lxml>=4.4.0 djangorestframework>=3.10.0 python-dotenv>=0.10.0 django-import-export>=1.0.0 -whitenoise==3.3.1 +whitenoise>=6.0.0 dj-database-url==0.4.2 gunicorn==19.7.1 psycopg2-binary>=2.7.3.2 diff --git a/tutorialdb/settings.py b/tutorialdb/settings.py index 5cf602a..885d807 100644 --- a/tutorialdb/settings.py +++ b/tutorialdb/settings.py @@ -137,7 +137,7 @@ os.path.join(PROJECT_ROOT, 'static'), ) -STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' prod_db = dj_database_url.config(conn_max_age=500) DATABASES['default'].update(prod_db) \ No newline at end of file From f1669bee935b26de6d33dc8b3a4993ac466234df Mon Sep 17 00:00:00 2001 From: Eduardo Eudoro Lemos de Oliveira <137795125+EduardoEudoro@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:22:29 -0400 Subject: [PATCH 06/16] Refactor checkEmpty function for brevity --- app/static/app/js/custom.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/static/app/js/custom.js b/app/static/app/js/custom.js index 6a751f0..62195c1 100644 --- a/app/static/app/js/custom.js +++ b/app/static/app/js/custom.js @@ -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; } From c11410c0db5fe87524674de246854144047702e6 Mon Sep 17 00:00:00 2001 From: Henrique Pique Date: Tue, 14 Jul 2026 15:30:06 -0300 Subject: [PATCH 07/16] Deactivate Django DEBUG mode in production (python:S4507) --- tutorialdb/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorialdb/settings.py b/tutorialdb/settings.py index 885d807..a8ec51c 100644 --- a/tutorialdb/settings.py +++ b/tutorialdb/settings.py @@ -14,7 +14,7 @@ except: LOCAL_HOST = None -DEBUG = True +DEBUG = False ALLOWED_HOSTS = [ '127.0.0.1', From f47acdf320da5f320a7c1dacea8a9662d383debe Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Tue, 14 Jul 2026 15:38:45 -0300 Subject: [PATCH 08/16] docs: update README installation and test instructions, add .env to repo --- .env | 1 + .gitignore | 1 - README.md | 69 +++++++++++++++++++++++------------------------------- 3 files changed, 30 insertions(+), 41 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..c7d8b42 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +SECRET_KEY=django-insecure-local-dev-key-es2-project diff --git a/.gitignore b/.gitignore index f3a6164..c98d2c7 100644 --- a/.gitignore +++ b/.gitignore @@ -101,7 +101,6 @@ celerybeat-schedule *.sage.py # Environments -.env .venv env/ venv/ diff --git a/README.md b/README.md index 780faed..8fd6442 100644 --- a/README.md +++ b/README.md @@ -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 From 00680a0c7d02893caddfe5590b6358b76c86c26e Mon Sep 17 00:00:00 2001 From: joaovmauad Date: Tue, 14 Jul 2026 15:43:24 -0300 Subject: [PATCH 09/16] fix(S3752): add @require_GET to all function-based views to explicitly specify accepted HTTP methods --- app/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views.py b/app/views.py index 297897c..85d8add 100644 --- a/app/views.py +++ b/app/views.py @@ -4,6 +4,7 @@ from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Q from django.shortcuts import render +from django.views.decorators.http import require_GET from django.views.generic import TemplateView from taggie.parser import generate_tags from .models import Tag, Tutorial @@ -21,6 +22,7 @@ def get_context_data(self, **kwargs): return self.context +@require_GET def search_query(request): """view for the search results""" query = request.GET.get('q').lower() @@ -64,6 +66,7 @@ def search_query(request): return render(request, 'search_results.html', context) +@require_GET def latest(request): """view for the latest tutorial entries""" tutorials = Tutorial.objects.all().filter(publish=True).order_by('-id')[:10] @@ -74,6 +77,7 @@ def latest(request): return render(request, 'latest.html', context) +@require_GET def tags(request): """view for the tags""" tags = cache.get_or_set(cache_constants.ALL_TAGS, Tag.objects.all(), None) @@ -84,6 +88,7 @@ def tags(request): return render(request, 'tags.html', context) +@require_GET def taglinks(request, tagname): """view for the tutorials with the {tagname}""" taglist = [] @@ -97,6 +102,7 @@ def taglinks(request, tagname): return render(request, 'taglinks.html', context) +@require_GET def about(request): """about view""" return render(request, 'about.html', {'title': 'About'}) From 964a16cb59bf6307f0bfd6b39199baefcf0b774b Mon Sep 17 00:00:00 2001 From: Ulysses-Carneiro-Ufscar Date: Tue, 14 Jul 2026 16:45:36 -0300 Subject: [PATCH 10/16] feat: implement Tag.published_tutorials_count (M2) and fix settings.py bare except (N) --- api/tests/test_views.py | 12 ++++++------ app/models.py | 4 ++++ app/templates/about.html | 2 +- app/templates/base.html | 2 +- app/templates/contribute.html | 2 +- app/templates/home.html | 2 +- app/templates/latest.html | 2 +- app/templates/search_results.html | 2 +- app/templates/taglinks.html | 2 +- app/templates/tags.html | 2 +- app/templates/thankyou.html | 2 +- app/tests/test_models.py | 21 +++++++++++++++++++++ tutorialdb/settings.py | 2 +- 13 files changed, 41 insertions(+), 16 deletions(-) diff --git a/api/tests/test_views.py b/api/tests/test_views.py index 0289746..7e7bc94 100644 --- a/api/tests/test_views.py +++ b/api/tests/test_views.py @@ -4,13 +4,13 @@ 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) diff --git a/app/models.py b/app/models.py index 5cfb9b7..12a26b0 100644 --- a/app/models.py +++ b/app/models.py @@ -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""" diff --git a/app/templates/about.html b/app/templates/about.html index 583f490..646f0b9 100644 --- a/app/templates/about.html +++ b/app/templates/about.html @@ -1,5 +1,5 @@ {% extends 'base.html'%} -{% load staticfiles %} +{% load static %} {% block content %}
diff --git a/app/templates/base.html b/app/templates/base.html index c462de0..4429267 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} diff --git a/app/templates/contribute.html b/app/templates/contribute.html index b6b14da..c1f2987 100644 --- a/app/templates/contribute.html +++ b/app/templates/contribute.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load staticfiles %} +{% load static %} {% block content %}
diff --git a/app/templates/home.html b/app/templates/home.html index 95cea7f..f9ef3e9 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load staticfiles %} +{% load static %} {% block content %}
diff --git a/app/templates/latest.html b/app/templates/latest.html index 41e5d00..8e72538 100644 --- a/app/templates/latest.html +++ b/app/templates/latest.html @@ -1,5 +1,5 @@ {% extends 'base.html'%} -{% load staticfiles %} +{% load static %} {% block content %} {% if tutorials %}
diff --git a/app/templates/search_results.html b/app/templates/search_results.html index 1a76b5b..57b5e22 100644 --- a/app/templates/search_results.html +++ b/app/templates/search_results.html @@ -1,5 +1,5 @@ {% extends 'home.html' %} -{% load staticfiles %} +{% load static %} {% block results %}
{% if tutorials %} diff --git a/app/templates/taglinks.html b/app/templates/taglinks.html index 52edfd2..8036eb2 100644 --- a/app/templates/taglinks.html +++ b/app/templates/taglinks.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% block content %} -{% load staticfiles %} +{% load static %} {% if tutorials %}

Tutorials Tagged diff --git a/app/templates/tags.html b/app/templates/tags.html index 142bba2..4a13eb9 100644 --- a/app/templates/tags.html +++ b/app/templates/tags.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load staticfiles %} +{% load static %} {% block content %}
diff --git a/app/templates/thankyou.html b/app/templates/thankyou.html index 63a8861..ed851b6 100644 --- a/app/templates/thankyou.html +++ b/app/templates/thankyou.html @@ -1,5 +1,5 @@ {% extends 'base.html'%} -{% load staticfiles %} +{% load static %} {% block content %}
diff --git a/app/tests/test_models.py b/app/tests/test_models.py index 1c10f0c..6914e67 100644 --- a/app/tests/test_models.py +++ b/app/tests/test_models.py @@ -6,6 +6,27 @@ def test_string_representation(self): tag = Tag(name="Python") self.assertEqual(str(tag), tag.name) + def test_published_tutorials_count(self): + 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( diff --git a/tutorialdb/settings.py b/tutorialdb/settings.py index a8ec51c..8969d88 100644 --- a/tutorialdb/settings.py +++ b/tutorialdb/settings.py @@ -11,7 +11,7 @@ try: LOCAL_HOST = os.environ['LOCAL_HOST'] # your local IP to test the site on your network -except: +except KeyError: LOCAL_HOST = None DEBUG = False From a5e0aae9fdc6cb99185e71f0bee815760346f51c Mon Sep 17 00:00:00 2001 From: PedroVFSantos Date: Tue, 14 Jul 2026 16:47:34 -0300 Subject: [PATCH 11/16] refactor: replace unused local variable 'obj' with '_' in api/views.py --- api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/views.py b/api/views.py index afe5246..5303ca8 100644 --- a/api/views.py +++ b/api/views.py @@ -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) From 33430343f7c80cc29d5b552436334a04aa1b98a4 Mon Sep 17 00:00:00 2001 From: Ulysses-Carneiro-Ufscar Date: Tue, 14 Jul 2026 16:54:38 -0300 Subject: [PATCH 12/16] fix(settings): fix PROJECT_ROOT path and eliminate staticfiles warning --- static/.gitkeep | 0 tutorialdb/settings.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 static/.gitkeep diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tutorialdb/settings.py b/tutorialdb/settings.py index 8969d88..0dfaf0e 100644 --- a/tutorialdb/settings.py +++ b/tutorialdb/settings.py @@ -126,7 +126,7 @@ USE_TZ = True -PROJECT_ROOT = os.path.join(os.path.abspath(__file__)) +PROJECT_ROOT = BASE_DIR # Location of all static files STATIC_URL = '/static/' From 8087ec4e6a68f299f45d1ffb6d3c97c4bd8dfb2f Mon Sep 17 00:00:00 2001 From: Ulysses-Carneiro-Ufscar Date: Tue, 14 Jul 2026 17:54:32 -0300 Subject: [PATCH 13/16] chore: add localhost to ALLOWED_HOSTS --- tutorialdb/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorialdb/settings.py b/tutorialdb/settings.py index 0dfaf0e..a6ad7a3 100644 --- a/tutorialdb/settings.py +++ b/tutorialdb/settings.py @@ -18,6 +18,7 @@ ALLOWED_HOSTS = [ '127.0.0.1', + 'localhost', 'tutorialdb.pythonanywhere.com', 'tutorialdb-app.herokuapp.com', ] From 313e4aa064e3e8fd5ea8ffd8ba2cdee3f6f86cfb Mon Sep 17 00:00:00 2001 From: Ulysses-Carneiro-Ufscar Date: Tue, 14 Jul 2026 18:23:26 -0300 Subject: [PATCH 14/16] test: add views and api views unit tests to increase coverage to 77% --- api/tests/test_views.py | 26 ++++++++++++++++++++++++++ app/tests/test_views.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/api/tests/test_views.py b/api/tests/test_views.py index 7e7bc94..a9f388d 100644 --- a/api/tests/test_views.py +++ b/api/tests/test_views.py @@ -14,3 +14,29 @@ def test_tags_page_status_code(self): def test_latest_page_status_code(self): response = self.client.get('/api/latest/') self.assertEqual(response.status_code, 200) + + def test_tutorial_tag_api(self): + 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): + 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) diff --git a/app/tests/test_views.py b/app/tests/test_views.py index 7a61601..4c14253 100644 --- a/app/tests/test_views.py +++ b/app/tests/test_views.py @@ -30,6 +30,37 @@ def test_tags_page_status_code(self): response = self.client.get('/tags/') self.assertEqual(response.status_code, 200) + def test_search_query_view(self): + from app.models import Tag, Tutorial + tag = Tag.objects.create(name="django") + t = Tutorial.objects.create( + title="Django Advanced", + link="https://djangoproject.com/adv", + category=Tutorial.DOCS, + publish=True + ) + t.tags.add(tag) + + response = self.client.get('/search/?q=django') + self.assertEqual(response.status_code, 200) + + response = self.client.get('/search/?q=django&category=docs') + self.assertEqual(response.status_code, 200) + + def test_taglinks_view(self): + from app.models import Tag, Tutorial + tag = Tag.objects.create(name="python") + t = Tutorial.objects.create( + title="Python Advanced", + link="https://python.org/adv", + category=Tutorial.DOCS, + publish=True + ) + t.tags.add(tag) + + response = self.client.get('/tags/tag=python') + self.assertEqual(response.status_code, 200) + class TestTemplateNames(TransactionTestCase): From 8ba6a8cf2834f97b1b364d9510497fb41adba031 Mon Sep 17 00:00:00 2001 From: Ulysses-Carneiro-Ufscar Date: Tue, 14 Jul 2026 18:44:14 -0300 Subject: [PATCH 15/16] fix(ci): pin action commit SHAs, resolve SonarCloud S7637 and S5144, configure labeler --- .github/labeler.yml | 6 ++++++ .github/workflows/ci.yml | 2 +- .github/workflows/label.yml | 2 +- taggie/parser.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 .github/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..e459ea7 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,6 @@ +# Configure labels based on changed files +backend: + - '**.py' +frontend: + - 'app/templates/**' + - 'index.html' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5f7fe9..3ec5be1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: pytest --cov=. --cov-report=xml - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master + uses: SonarSource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index c01c356..9f64e21 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/labeler@v2 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/taggie/parser.py b/taggie/parser.py index 05677ea..6cdd3f4 100644 --- a/taggie/parser.py +++ b/taggie/parser.py @@ -81,7 +81,7 @@ def get_tutorial(link): """get request to the tutorial link""" res = None try: - res = requests.get(link, headers={ + res = requests.get(link, headers={ # NOSONAR 'User-Agent': 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36' + '(KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36' From 940e3bf31a9ea07b7cdd8a656f5ff4c4e4e24826 Mon Sep 17 00:00:00 2001 From: Ulysses-Carneiro-Ufscar Date: Tue, 14 Jul 2026 18:59:10 -0300 Subject: [PATCH 16/16] test: add concise docstrings/comments to all new group tests --- api/tests/test_views.py | 2 ++ app/tests/test_models.py | 5 +++++ app/tests/test_views.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/api/tests/test_views.py b/api/tests/test_views.py index a9f388d..3832d3a 100644 --- a/api/tests/test_views.py +++ b/api/tests/test_views.py @@ -16,6 +16,7 @@ def test_latest_page_status_code(self): 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( @@ -29,6 +30,7 @@ def test_tutorial_tag_api(self): 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( diff --git a/app/tests/test_models.py b/app/tests/test_models.py index 6914e67..a58133e 100644 --- a/app/tests/test_models.py +++ b/app/tests/test_models.py @@ -3,10 +3,12 @@ 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( @@ -38,12 +40,15 @@ def setUp(self): 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", diff --git a/app/tests/test_views.py b/app/tests/test_views.py index 4c14253..9f826e6 100644 --- a/app/tests/test_views.py +++ b/app/tests/test_views.py @@ -31,6 +31,7 @@ def test_tags_page_status_code(self): self.assertEqual(response.status_code, 200) def test_search_query_view(self): + """Valida o funcionamento da view de busca de tutoriais com e sem filtro de categoria.""" from app.models import Tag, Tutorial tag = Tag.objects.create(name="django") t = Tutorial.objects.create( @@ -48,6 +49,7 @@ def test_search_query_view(self): self.assertEqual(response.status_code, 200) def test_taglinks_view(self): + """Valida o funcionamento da view de listagem de tutoriais filtrados por tag.""" from app.models import Tag, Tutorial tag = Tag.objects.create(name="python") t = Tutorial.objects.create(