Skip to content

Commit 491a827

Browse files
committed
Merge remote-tracking branch 'origin/main' into development
# Conflicts: # .github/workflows/cicd.yml # Pipfile # Pipfile.lock # db.sqlite3 # myrecommendations/settings_render.py
2 parents 1b00e6d + 6d13deb commit 491a827

14 files changed

Lines changed: 614 additions & 468 deletions

File tree

.github/workflows/cicd.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ jobs:
2323
pipenv install --dev
2424
- name: Run Tests
2525
run: |
26-
pipenv run python manage.py test
27-
pipenv run python manage.py collectstatic --noinput
28-
pipenv run python manage.py behave
26+
python manage.py test
27+
python manage.py collectstatic --noinput
28+
python manage.py behave

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ ENV PYTHONUNBUFFERED 1
33
RUN mkdir /code
44
WORKDIR /code
55

6-
COPY Pipfile Pipfile.lock ./
7-
RUN pip install pipenv
8-
RUN pipenv install --system --deploy
6+
COPY pyproject.toml ./
7+
RUN pip install uv
8+
RUN uv pip install --system -r pyproject.toml
99

1010
COPY myrecommendations ./myrecommendations
1111
COPY myrestaurants ./myrestaurants

Pipfile

Lines changed: 0 additions & 22 deletions
This file was deleted.

Pipfile.lock

Lines changed: 0 additions & 363 deletions
This file was deleted.

Procfile

Lines changed: 0 additions & 1 deletion
This file was deleted.

README.md

Lines changed: 26 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Starting the MyRecommendations Project from Scratch
2828
===================================================
2929

3030
First of all, install the latest version of Python from [downloads](https://www.python.org/downloads/) and
31-
[pipenv](https://pipenv.readthedocs.io/en/latest/install/) to help you manage dependencies and virtual environments.
31+
[uv](https://docs.astral.sh/uv/getting-started/installation/) to help you manage dependencies and virtual environments.
3232

3333
Then, create the folder for the new project, in our case the project is called 'myrecommendations':
3434

@@ -38,13 +38,15 @@ $ mkdir myrecommendations
3838
$ cd myrecommendations
3939
```
4040

41-
Once in the `myrecommendations` folder, activate the pipenv virtual environment to keep the Python packages
41+
Once in the `myrecommendations` folder, create a uv virtual environment to keep the Python packages
4242
for your project organised and start by installing Django. Then, create a new Django project:
4343

4444
```shell script
45-
$ pipenv shell
45+
$ uv venv
4646

47-
$ pipenv install Django
47+
$ source .venv/bin/activate # On Windows use: .venv\Scripts\activate
48+
49+
$ uv pip install Django
4850

4951
$ django-admin startproject myrecommendations .
5052
```
@@ -174,8 +176,8 @@ To facilitate the description of the feature scenarios, while connecting them to
174176
To get Behave and integrate it with Django, install:
175177

176178
```shell script
177-
$ pipenv install behave
178-
$ pipenv install behave-django
179+
$ uv pip install behave
180+
$ uv pip install behave-django
179181
```
180182

181183
And add the 'behave_django' application at the end of the INSTALLED_APPS list in *myrecommendations/settings.py*:
@@ -196,52 +198,10 @@ INSTALLED_APPS = [
196198
Moreover, to make it possible to guide a browser from the test, and thus check if the application follows the expected behaviour from a end-user perspective, we will also use Splinter. It can be installed with the following command:
197199

198200
```shell script
199-
$ pipenv install splinter
200-
```
201-
202-
Finally, for end-to-end test, it is necessary to have a browser to test from client side. With Splinter, different browsers can be configured for testing, for instance Chrome, the most common one.
203-
204-
Assuming Chrome is already installed in your computer, the only requirement to use it for automated testing is the ChromeDriver, available for Windows, Linux and Mac from [https://sites.google.com/a/chromium.org/chromedriver/downloads](https://sites.google.com/a/chromium.org/chromedriver/downloads)
205-
206-
You can also install it using different package managers. For instance with **apt** on Linux:
207-
208-
```shell script
209-
apt-get update
210-
apt-get install chromedriver
211-
```
212-
213-
Or **[Brew](https://brew.sh)** on OSX:
214-
215-
```shell script
216-
brew update
217-
brew install chromedriver
218-
xattr -d com.apple.quarantine $(which chromedriver)
219-
```
220-
221-
Or **[Chocolatey](https://chocolatey.org/docs/installation)** on Windows:
222-
223-
```shell script
224-
choco install chromedriver
201+
$ uv pip install splinter selenium
225202
```
226203

227-
### Testing with Firefox instead of Chrome ###
228-
229-
Alternatively, for Firefox, install the browser:
230-
231-
```
232-
sudo apt-get update
233-
sudo apt-get install firefox
234-
```
235-
236-
Then, download Geckodriver and unpack it from [https://github.com/mozilla/geckodriver/releases/](https://github.com/mozilla/geckodriver/releases/)
237-
238-
Finally, add the geckodriver to your path:
239-
240-
```
241-
export PATH=$PATH:/your/path/to/geckodriver
242-
```
243-
244-
And configure the test environment in `environment.py`, as detailed next, to use `firefox` instead of `chrome`.
204+
Finally, for end-to-end test, it is necessary to have a browser to test from client side. With Splinter and Selenium, different browsers can be configured for testing, for instance Chrome or Firefox, which should be installed in the machine where the tests are run.
245205

246206
## Environment ##
247207

@@ -254,6 +214,7 @@ from splinter.browser import Browser
254214

255215
def before_all(context):
256216
context.browser = Browser('chrome', headless=True)
217+
# Alternatively, use `firefox` and headless=False to see the browser while testing
257218

258219
def after_all(context):
259220
context.browser.quit()
@@ -362,6 +323,15 @@ def step_impl(context, username, password):
362323
context.browser.fill('password', password)
363324
form.find_by_value('login').first.click()
364325
assert context.browser.is_text_present('User: ' + username)
326+
327+
@then("I'm redirected to the login form")
328+
def step_impl(context):
329+
wait = WebDriverWait(context.browser.driver, 3)
330+
login_form = wait.until(
331+
visibility_of_element_located((By.ID, "login-form"))
332+
)
333+
# context.browser.driver.save_screenshot("debug_redirect.png")
334+
assert context.browser.url.startswith(context.get_url('login'))
365335
```
366336

367337
Both steps are parameterized, so we can use the steps to login users with any username and password, as long as we have previously created them and the password matches.
@@ -448,10 +418,6 @@ The undefined steps are related with the Register Restaurant feature and are imp
448418

449419
```python
450420
from behave import *
451-
import operator
452-
from functools import reduce
453-
from django.db.models import Q
454-
455421
use_step_matcher("parse")
456422

457423
@when(u'I register restaurant')
@@ -463,12 +429,13 @@ def step_impl(context):
463429
context.browser.fill(heading, row[heading])
464430
form.find_by_value('Submit').first.click()
465431

466-
@then(u'I\'m viewing the details page for restaurant by "{user}"')
432+
@then(u'I\'m viewing the details page for restaurant by "{username}"')
467433
def step_impl(context, user):
468-
q_list = [Q((attribute, context.table.rows[0][attribute])) for attribute in context.table.headings]
469-
from myrestaurants.models import Restaurant
470-
restaurant = Restaurant.objects.filter(reduce(operator.and_, q_list)).get()
471-
assert context.browser.url == context.get_url(restaurant)
434+
context.browser.is_text_present(username)
435+
for row in context.table:
436+
for heading in row.headings:
437+
expected = row[heading]
438+
context.browser.is_text_present(expected)
472439

473440
@then(u'There are {count:n} restaurants')
474441
def step_impl(context, count):

db.sqlite3

0 Bytes
Binary file not shown.

features/environment.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,47 @@
1+
import re
2+
import os
13
from splinter.browser import Browser
24

5+
def chrome_browser(headless=True):
6+
from selenium.webdriver.chrome.options import Options
7+
chrome_options = Options()
8+
if headless:
9+
chrome_options.add_argument("--headless=new") # recommended headless mode
10+
chrome_options.add_argument("--disable-notifications")
11+
chrome_options.add_argument("--disable-infobars")
12+
chrome_options.add_argument("--no-sandbox")
13+
chrome_options.add_argument("--disable-dev-shm-usage")
14+
chrome_options.add_argument("--window-size=1920,1080")
15+
prefs = {
16+
"credentials_enable_service": False, # disable password manager
17+
"profile.password_manager_enabled": False,
18+
"profile.password_manager_leak_detection": False
19+
}
20+
chrome_options.add_experimental_option("prefs", prefs)
21+
return Browser("chrome", options=chrome_options)
22+
23+
def firefox_browser(headless=True):
24+
return Browser("firefox", headless=headless)
25+
326
def before_all(context):
4-
context.browser = Browser('chrome', headless=True)
27+
context.browser = chrome_browser(headless=True)
28+
# Alternatively, use `firefox_browser` and headless=False to see the browser while testing
529

630
def after_all(context):
731
context.browser.quit()
832
context.browser = None
33+
34+
def slugify(text):
35+
text = text.lower()
36+
text = re.sub(r"[^\w\s-]", "", text)
37+
text = re.sub(r"[\s]+", "-", text)
38+
return text
39+
40+
def after_step(context, step):
41+
if step.status == "failed":
42+
feature = slugify(context.feature.name)
43+
scenario = slugify(context.scenario.name)
44+
step_name = slugify(step.name)
45+
os.makedirs("screenshots", exist_ok=True)
46+
filename = f"screenshots/{feature}__{scenario}__{step_name}.png"
47+
context.browser.driver.save_screenshot(filename)

features/steps/authentication.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from selenium.webdriver.common.by import By
12
from behave import *
3+
from selenium.webdriver.support.expected_conditions import visibility_of_element_located
4+
from selenium.webdriver.support.wait import WebDriverWait
25

36
use_step_matcher("parse")
47

@@ -35,4 +38,8 @@ def step_impl(context, link_text):
3538

3639
@then("I'm redirected to the login form")
3740
def step_impl(context):
41+
wait = WebDriverWait(context.browser.driver, 3)
42+
login_form = wait.until(
43+
visibility_of_element_located((By.ID, "login-form"))
44+
)
3845
assert context.browser.url.startswith(context.get_url('login'))

features/steps/register_dish.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,12 @@ def step_impl(context, restaurant_name):
4040

4141
@then('I\'m viewing the details page for dish at restaurant "{restaurant_name}" by "{username}"')
4242
def step_impl(context, restaurant_name, username):
43-
q_list = [Q((attribute, context.table.rows[0][attribute])) for attribute in context.table.headings]
44-
from django.contrib.auth.models import User
45-
q_list.append(Q(('user', User.objects.get(username=username))))
46-
from myrestaurants.models import Restaurant
47-
q_list.append(Q(('restaurant', Restaurant.objects.get(name=restaurant_name))))
48-
from myrestaurants.models import Dish
49-
dish = Dish.objects.filter(reduce(operator.and_, q_list)).get()
50-
assert context.browser.url == context.get_url(dish)
51-
if dish.image:
52-
dish.image.delete()
43+
context.browser.is_text_present(username)
44+
context.browser.is_text_present(restaurant_name)
45+
for row in context.table:
46+
for heading in row.headings:
47+
expected = row[heading]
48+
context.browser.is_text_present(expected)
5349

5450
@then('There are {count:n} dishes')
5551
def step_impl(context, count):

0 commit comments

Comments
 (0)