diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..80249ad --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "image":"mcr.microsoft.com/devcontainers/universal:2", + "customizations": { + "vscode": { + "extensions": [ + "GitHub.copilot" + ] + } + }, + "postCreateCommand": "bash -i .devcontainer/install-dependencies.sh" + +} \ No newline at end of file diff --git a/.devcontainer/install-dependencies.sh b/.devcontainer/install-dependencies.sh new file mode 100644 index 0000000..aa669ed --- /dev/null +++ b/.devcontainer/install-dependencies.sh @@ -0,0 +1,6 @@ + +cd ./copilot + +python -m pip install -r requirements.txt + +python manage.py migrate diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e78710 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +## Overview + +This project is a Django-based application designed to serve as a learning platform for utilizing GitHub Copilot in software development. The application features a series of labs that guide users through the process of creating API routes, generating sample data, and testing APIs. The project aims to demonstrate the power of GitHub Copilot in streamlining development tasks and enhancing productivity. + +## Getting Started +Before diving into the labs, ensure you have Django installed and the project's dependencies are set up. Navigate to the project's root directory and run the following commands: + +```bash +cd copilot +python -m pip install -r requirements.txt +``` + +To start the server, execute: + +```bash +python manage.py runserver +``` + +## Labs Overview + +### [Lab 1: Implement New Route](./docs/001-implement-new-route.md) + +In this lab, participants will learn how to add a new API route to the Django application. The new route will return a simple "Hello World" JSON response. This lab also includes writing tests for the new route to ensure it behaves as expected. + +### [Lab 2: Data and Services](./docs/002-data-and-services.md) + +This lab focuses on generating sample data for the application. Participants will create an API that lists Microsoft Azure VMs information, fetched from a local JSON file. This lab covers the entire flow from generating the sample data to testing the new API endpoint. + +### [Lab 3: Create Homepage](/docs/003-create-homepage.md) +The details for Lab 3 are not provided in the context. However, based on the naming convention, it's likely focused on creating a homepage for the Django application, possibly involving front-end development aspects and integrating the APIs developed in previous labs. + +### Testing + +The project includes a suite of tests to validate the functionality of the API routes. To run the tests, ensure the server is not running and execute: + +```bash +python manage.py test +``` + +## Conclusion + +This project serves as a practical guide to leveraging GitHub Copilot in developing and testing web applications. Through a series of hands-on labs, participants will gain insights into efficient coding practices and automated testing strategies. + + diff --git a/copilot/api/tests.py b/copilot/api/tests.py index 7ce503c..936db41 100644 --- a/copilot/api/tests.py +++ b/copilot/api/tests.py @@ -1,3 +1,73 @@ -from django.test import TestCase +from rest_framework.test import APITestCase +from rest_framework.utils import json -# Create your tests here. +class TimeAPITestCase(APITestCase): + api_path = '/api/time/' + + def test_get_current_time(self): + response = self.client.get(self.api_path) + self.assertEqual(response.status_code, 200) + response_data = json.loads(response.content) + self.assertIn('time', response_data) + self.assertIsNotNone(response_data['time']) + +class HelloAPITestCase(APITestCase): + api_path = '/api/hello/' + + def test_get_hello(self): + response = self.client.get(self.api_path) + self.assertEqual(response.status_code, 501) + response_data = json.loads(response.content) + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], 'key query parameter is required') + + def test_get_hello_with_key(self): + response = self.client.get(self.api_path, {'key': 'World'}) + self.assertEqual(response.status_code, 200) + response_data = json.loads(response.content) + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], 'Hello World') + + +class VmsAPITestCase(APITestCase): + api_path = '/api/vms/' + + def test_get_vms(self): + response = self.client.get(self.api_path) + + # Check if the response status code is 200 + self.assertEqual(response.status_code, 200) + + # The response content should be an array + response_data = json.loads(response.content) + self.assertIsInstance(response_data, list) + + +# Additional tests for VmsDataAPITestCase to validate the API based on the data in vms.json + +class VmsDataAPITestCase(APITestCase): + api_path = '/api/vms/' + + # Existing test_get_vms method... + + def test_vms_data_validation(self): + expected_vms_data = [ + {"size": "Standard_D2_v3", "vcpu": 2, "memory": 8}, + {"size": "Standard_D4_v3", "vcpu": 4, "memory": 16}, + {"size": "Standard_D8_v3", "vcpu": 8, "memory": 32}, + {"size": "Standard_D16_v3", "vcpu": 16, "memory": 64}, + {"size": "Standard_D32_v3", "vcpu": 32, "memory": 128}, + {"size": "Standard_D48_v3", "vcpu": 48, "memory": 192}, + {"size": "Standard_D64_v3", "vcpu": 64, "memory": 256}, + ] + + response = self.client.get(self.api_path) + self.assertEqual(response.status_code, 200) + response_data = json.loads(response.content) + + # Ensure the response contains the correct number of VMs + self.assertEqual(len(response_data), len(expected_vms_data)) + + # Validate each VM's data + for vm_data in expected_vms_data: + self.assertIn(vm_data, response_data) \ No newline at end of file diff --git a/copilot/api/urls.py b/copilot/api/urls.py index e3e0a93..6397aee 100644 --- a/copilot/api/urls.py +++ b/copilot/api/urls.py @@ -5,4 +5,6 @@ urlpatterns = [ path('time/', views.get_current_time), + path('hello/', views.get_hello), + path('vms/', views.get_vms), ] diff --git a/copilot/api/views.py b/copilot/api/views.py index 4c8615d..52faedd 100644 --- a/copilot/api/views.py +++ b/copilot/api/views.py @@ -3,9 +3,35 @@ from rest_framework.exceptions import NotFound from rest_framework.parsers import JSONParser from rest_framework.decorators import api_view +import json + import datetime @api_view(['GET']) def get_current_time(request): current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - return JsonResponse({'time': current_time}, status=status.HTTP_200_OK) \ No newline at end of file + return JsonResponse({'time': current_time}, status=status.HTTP_200_OK) + + +# Create a new function GET hello?key=World +# that returns a JSON {"message": "Hello World"} when the query parameter key is present +# and return HTTP 501 code with message "key query parameter is required" +# when the query parameter key is not present +@api_view(['GET']) +def get_hello(request): + key = request.GET.get('key') + if key: + return JsonResponse({'message': f'Hello {key}'}, status=status.HTTP_200_OK) + else: + return JsonResponse({'message': 'key query parameter is required'}, status=status.HTTP_501_NOT_IMPLEMENTED) + + +@api_view(['GET']) +def get_vms(request): + try: + with open('./data/vms.json', 'r') as file: + data = json.load(file) + return JsonResponse(data, safe=False, status=status.HTTP_200_OK) + except FileNotFoundError: + return JsonResponse({'message': 'File not found'}, status=status.HTTP_404_NOT_FOUND) + diff --git a/copilot/copilot/settings.py b/copilot/copilot/settings.py index c804b3c..6a4d4a4 100644 --- a/copilot/copilot/settings.py +++ b/copilot/copilot/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -56,7 +57,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'copilot/templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ diff --git a/copilot/copilot/templates/home.html b/copilot/copilot/templates/home.html new file mode 100644 index 0000000..9e044fe --- /dev/null +++ b/copilot/copilot/templates/home.html @@ -0,0 +1,43 @@ + + + + + + VM List + + + + +
+

VM List

+ + + + + + + + + +
SizevCPUMemory (GB)
+
+ + + + + + + + \ No newline at end of file diff --git a/copilot/copilot/tests.py b/copilot/copilot/tests.py index 3ea6c46..e69de29 100644 --- a/copilot/copilot/tests.py +++ b/copilot/copilot/tests.py @@ -1,12 +0,0 @@ -from rest_framework.test import APITestCase -from rest_framework.utils import json - -class TimeAPITestCase(APITestCase): - api_path = '/api/time/' - - def test_get_current_time(self): - response = self.client.get(self.api_path) - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.content) - self.assertIn('time', response_data) - self.assertIsNotNone(response_data['time']) diff --git a/copilot/copilot/urls.py b/copilot/copilot/urls.py index d7371d0..1911919 100644 --- a/copilot/copilot/urls.py +++ b/copilot/copilot/urls.py @@ -18,8 +18,10 @@ from django.urls import path from django.urls.conf import include from api import urls +from .views import home urlpatterns = [ + path('', home), path('api/', include(urls)), path('admin/', admin.site.urls), ] diff --git a/copilot/copilot/views.py b/copilot/copilot/views.py new file mode 100644 index 0000000..0b21058 --- /dev/null +++ b/copilot/copilot/views.py @@ -0,0 +1,4 @@ +from django.shortcuts import render + +def home(request): + return render(request, 'home.html') \ No newline at end of file diff --git a/copilot/data/vms.json b/copilot/data/vms.json new file mode 100644 index 0000000..623c9ef --- /dev/null +++ b/copilot/data/vms.json @@ -0,0 +1,37 @@ +[ + { + "size": "Standard_D2_v3", + "vcpu": 2, + "memory": 8 + }, + { + "size": "Standard_D4_v3", + "vcpu": 4, + "memory": 16 + }, + { + "size": "Standard_D8_v3", + "vcpu": 8, + "memory": 32 + }, + { + "size": "Standard_D16_v3", + "vcpu": 16, + "memory": 64 + }, + { + "size": "Standard_D32_v3", + "vcpu": 32, + "memory": 128 + }, + { + "size": "Standard_D48_v3", + "vcpu": 48, + "memory": 192 + }, + { + "size": "Standard_D64_v3", + "vcpu": 64, + "memory": 256 + } +] \ No newline at end of file diff --git a/copilot/requirements.txt b/copilot/requirements.txt new file mode 100644 index 0000000..969d744 --- /dev/null +++ b/copilot/requirements.txt @@ -0,0 +1,4 @@ +asgiref==3.8.1 +Django==5.0.6 +djangorestframework==3.15.1 +sqlparse==0.5.0 diff --git a/docs/001-implement-new-route.md b/docs/001-implement-new-route.md new file mode 100644 index 0000000..0f151b1 --- /dev/null +++ b/docs/001-implement-new-route.md @@ -0,0 +1,77 @@ + +# New API Routes + +>If you are note using Codespaces, make sure you run +> +> ``` +> cd copilot +> +> python -m pip install -r requirements.txt +>``` +> +> before starting this exercise. + +You can start the server by running, in the `$PROJECT_ROOT/copilot` directory the following command + +```bash +python manage.py runserver +``` + +The project is a Django project with a single app called `api` that contains a single route `/api/time` that returns the current date time as JSON. + +The current route return the current Date Time as JSON, for example open a terminal and run: + +```bash +curl http://localhost:8000/api/time/ +``` + +This should return the current date time in JSON format. + +## 001 Add a new route "Hello World" + +`curl -L http://localhost:8000/api/hello?key=World` + +should return the JSON + +`{"message": "Hello World"}` + +and should return a `HTTP 500` code when the query parameter `hello` is not present. + +The test is already written in the file `copilot/api/tests.py` and it is failing, you are done when the test pass. + +To run the tests, open a terminal and run the following command: + +```bash +cd copilot + +python manage.py test +``` + +
+ +Possible Flow + +1. Open the file `./copilot/api/views.py` + +2. Add a new route to the file using a simple comment for example + +```python +# Create a new function GET hello?key=World +# that returns a JSON {"message": "Hello World"} when the query parameter key is present +# and return HTTP 500 code with message "key query parameter is required" +# when the query parameter key is not present +``` + +3. Keep the views file opened and open the `./copilot/api/urls.py` file + +4. Add a comment to the file to ask Copilot to add the new route for get_hello function + + + +3. Let the code be generated from the comment + +> Note: it is true that the comment is longer that the code, but this is done to learn how to use copilot and understand the importance of being precise in the "prompt". + +
+ +--- diff --git a/docs/002-data-and-services.md b/docs/002-data-and-services.md new file mode 100644 index 0000000..ff67992 --- /dev/null +++ b/docs/002-data-and-services.md @@ -0,0 +1,168 @@ +# Generate Sample Data + +In this lab you will learn how to use Copilot to generate sample data for your application. + + +## 001 Create a new API that list Microsoft Azure VMs informations + +Add a new route `http://localhost:8000/api/azure-vms` that returns a list of VMs with some properties, that will come from a local JSON file. + + +when calling: + +```bash +curl http://localhost:8000/api/azure-vms +``` + +The API should return a list of VMs with the following properties: + +```json +[ + ... + { + "size": "Standard_D32_v3", + "vcpu": 32, + "memory": 128, + }, + .... + { + "size": "Standard_D64_v3", + "vcpu": 64, + "memory": 256, + } +] +``` + + +You can find the VMs properties here https://learn.microsoft.com/en-us/azure/virtual-machines/dv3-dsv3-series?source=recommendations + +> Tip: see how you can format the data in the chat using a simple copy/paste from the website. + + +
+ +Possible Flow + +1. Generate the data from the website + - Go to https://learn.microsoft.com/en-us/azure/virtual-machines/dv3-dsv3-series?source=recommendations + - Copy the data from the table + +2. Ask the following question in the chat + - _Using the following data, create a JSON Array, with the fields Size, vCPU and Memory. Put the field name in lowercase. The Memory field should be a number without unit (since the default is GiB_ + - Paste the content from Wikipedia in the chat + - This should generate a new JSON array + + +3. Click in the [...] button in the chat and select "Insert into New File" + +4. Create a the file : `$PROJECT_HOME/copilot/data/vms.json` + +6. Open the file `$PROJECT_HOME/copilot/views.py` and add the following code + - In the chat enter the following question : + - _Create a new GET view that read the ./data/vms.json file and return the JSON content_ + - The code should be something like this: + ```python + @api_view(['GET']) + def get_vms(request): + try: + with open('./data/vms.json', 'r') as file: + data = json.load(file) + return JsonResponse(data, safe=False, status=status.HTTP_200_OK) + except FileNotFoundError: + return JsonResponse({'message': 'File not found'}, status=status.HTTP_404_NOT_FOUND) + ``` + +8. Open the file `$PROJECT_HOME/copilot/urls.py` + - Go to the end of the list of urls, and add the new one, + - Copilot should complete the code for you + ```python + urlpatterns = [ + path('time/', views.get_current_time), + path('hello/', views.get_hello), + path('vms/', views.get_vms), + ] + ``` + + +9. In the terminal: + - Make sure you are in the directory `/copilot-rest-python/copilot/` + - Restart Django + ```bash + python manage.py runserver + ``` + - Go to the browser and access the URL `http://localhost:8000/api/vms/` + + - You will probably have an error, if you have an error, go in the terminal select the error message and do: + + - right-click > `Copilot : Explain this` + +10. When Fixed restart the server and test the API again + +
+ + +## 002 Add a test for the new API + +Add new tests for the new API that list Microsoft Azure VMs informations. + +
+ +Possible Flow + +1. Open the file `$PROJECT_HOME/copilot/tests.py` and add the following code + + - Use the inline completion to write a new test + - Something like : + + ```python + class VmsAPITestCase(APITestCase): + api_path = '/api/vms/' + + def test_get_vms(self): + response = self.client.get(self.api_path) + + # Check if the response status code is 200 + self.assertEqual(response.status_code, 200) + + # The response content should be an array + response_data = json.loads(response.content) + self.assertIsInstance(response_data, list) + ``` + + +2. Go in the Chat, and ask the following question to test some values + + - Ask the following question using the `#file` command : + - _Add some tests in #file:tests.py that validates the API based on the data found in #file:vms.json_ + + - The generated test could look like + ```python + class VmsDataAPITestCase(APITestCase): + api_path = '/api/vms/' + + # Existing test_get_vms method... + + def test_vms_data_validation(self): + expected_vms_data = [ + {"size": "Standard_D2_v3", "vcpu": 2, "memory": 8}, + {"size": "Standard_D4_v3", "vcpu": 4, "memory": 16}, + {"size": "Standard_D8_v3", "vcpu": 8, "memory": 32}, + {"size": "Standard_D16_v3", "vcpu": 16, "memory": 64}, + {"size": "Standard_D32_v3", "vcpu": 32, "memory": 128}, + {"size": "Standard_D48_v3", "vcpu": 48, "memory": 192}, + {"size": "Standard_D64_v3", "vcpu": 64, "memory": 256}, + ] + + response = self.client.get(self.api_path) + self.assertEqual(response.status_code, 200) + response_data = json.loads(response.content) + + # Ensure the response contains the correct number of VMs + self.assertEqual(len(response_data), len(expected_vms_data)) + + # Validate each VM's data + for vm_data in expected_vms_data: + self.assertIn(vm_data, response_data) + ``` + +
\ No newline at end of file diff --git a/docs/003-001.png b/docs/003-001.png new file mode 100644 index 0000000..8dd385b Binary files /dev/null and b/docs/003-001.png differ diff --git a/docs/003-create-homepage.md b/docs/003-create-homepage.md new file mode 100644 index 0000000..99924e4 --- /dev/null +++ b/docs/003-create-homepage.md @@ -0,0 +1,34 @@ +# Generate Sample Data + +Create a home page that show the list of VMs when accessing the url `http://localhost:8000/` + +The page should look like: + +![Home Page](./003-001.png) + + +
+ +Possible Flow + +1. Open the chat, click on the [+] at the top of the chat to starte new conversation + +2. Ask the following question: + + > @workspace Can you guide me to the steps to : + > + > - create a new HTML home page for the project + > - This page should use the Boostrap style and API + > - The API should call the VM rest API define in file:#file:urls.py #file:views.py + > - print the list of VMs in a HTML table with alternate colors + +2. This will give you the steps to create the home page, you can click on the + - [...] and "Open in New Editor" to open the file in the editor, and make it easier to read the steps + + +3. If you have any issue you can use the chat to ask for help or clarification, or look at the final solution in the `solutions` branch: + - https://github.com/github-copilot-workshop/copilot-rest-python/tree/solutions + + +
+