Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search day-care functionality to dog-owner homepage. #89

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def daycare_data():
pytest.DESCRIPTION = "This is the first daycare test"
pytest.PRICE_PER_DAY = 10
pytest.CAPACITY = 50
pytest.AREA = "Merkaz"
pytest.AREA = "C"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why not using the full name? 🙂

pytest.CITY = "Tel-Aviv"
pytest.ADDRESS = "The best street 5"

Expand Down
25 changes: 25 additions & 0 deletions daycare/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django import forms
from .models import Area


class DayCareSearchForm(forms.Form):
start_date = forms.DateField(required=True, label=' Start date',
widget=forms.widgets.DateInput(attrs={'class': 'form-control', 'type': 'date'}))
end_date = forms.DateField(required=True, label=' End date',
widget=forms.widgets.DateInput(attrs={'class': 'form-control', 'type': 'date'}))
area = forms.ChoiceField(required=False, label='Area',
choices=(Area.choices + [('', 'All'), ]), initial="")
city = forms.CharField(required=False, label='City')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a list of cities? a free-text would be tough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see #89 (comment) and let me know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you sum it up? I'm not sure I'm following. Anyway free-text can lead to issues (e.g: פתח-תקוה, פתח תקווה, פתח-תקווה, פתח תקוה)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List of cities seems to me good solution for static-users ,but because all users are static we can control the cities input and just avoid mistakes like that.
If we will want to support registration in the future it might limit us .
I saw django-cities(see https://pypi.org/project/django-cities/), it seems to have benefits of global support or even local but with more percise data.
I have no problem to create list of cities just want to know your opinion about it . @Omeramsc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI is an entry point for this app and as such it should perform validation on the input,
regardless if the users are static or not.
Nothing prevents for the next dev to create another static user and enter non valid city name, right?
If this is a lot of work you can do it on a follow up PR and write in the PR description about it.

name = forms.CharField(required=False, label='Day care name')
price_per_day = forms.IntegerField(required=False, label='Max price')

def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")

if start_date and end_date:
if end_date < start_date:
self._errors['end_date'] = self.error_class(["End date should be greater!"])

return cleaned_data
20 changes: 20 additions & 0 deletions daycare/migrations/0006_alter_daycare_area.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.0.3 on 2022-04-27 13:37
Yuval-Vino marked this conversation as resolved.
Show resolved Hide resolved

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('daycare', '0005_image_data_migration'),
]

operations = [
migrations.AlterField(
model_name='daycare',
name='area',
field=models.CharField(blank=True, choices=[('N', 'NORTH'), ('S', 'SOUTH'), ('C', 'CENTER')],
max_length=20, validators=[django.core.validators.MaxLengthValidator]),
),
]
8 changes: 7 additions & 1 deletion daycare/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
from django.core.exceptions import ObjectDoesNotExist


class Area(models.TextChoices):
North = 'N', 'NORTH'
South = 'S', 'SOUTH'
Center = 'C', 'CENTER'


class DayCare(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, default=None, blank=True, null=False, editable=True)
name = models.CharField(max_length=20, blank=True, unique=True, validators=[MaxLengthValidator])
description = models.TextField(blank=True, null=True)
price_per_day = models.IntegerField(blank=False, null=False, default=0)
capacity = models.IntegerField(null=False, blank=True)
area = models.CharField(max_length=20, blank=True, validators=[MaxLengthValidator])
area = models.CharField(max_length=20, blank=True, validators=[MaxLengthValidator], choices=Area.choices)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we include that in some of the daycare fixtures?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Area field in create_day_care_user from the confest been updated.

city = models.CharField(max_length=20, blank=True, validators=[MaxLengthValidator])
address = models.CharField(max_length=50, blank=True, validators=[MaxLengthValidator])

Expand Down
63 changes: 47 additions & 16 deletions dogowner/templates/dogowner/dog_owner_homepage.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,57 @@
{% extends "main/base_template.html" %}
{% load crispy_forms_tags %}
{% load static %}

{% block stylesheets %}
<link rel="stylesheet" href="{% static 'CSS/dog_owner_homepage.css' %}">
{% endblock %}

{% block content %}
<div class="row row-cols-1 g-3 cards">
<div style="display: flex; flex-direction: row; flex-wrap: wrap">
{% for daycare in daycares %}
<div class="card">
<img src="{{ daycare.get_daycare_primary_image_url }}" alt="{{ daycare.name }} image" class="card-img-top">
<div class="card-body">
<div style="display: flex; flex-direction: row">
<h5 class="card-title">{{ daycare.name }}</h5>

<div class="row">
<form method='POST' action=''>{% csrf_token %}
<div class="col-4" id="searchBoxArea">
<span>Search for daycare:</span>
<div class="form-group">
{{ form.start_date|as_crispy_field }}
</div>
<div class="form-group">
{{ form.end_date|as_crispy_field }}
</div>
<div class="form-group">
{{ form.price_per_day|as_crispy_field }}
</div>
<div class="form-group">
{{ form.area|as_crispy_field }}
</div>
<div class="form-group">
{{ form.city|as_crispy_field }}
</div>
<div class="form-group">
{{ form.name|as_crispy_field }}
</div>
<input type="submit" value='Search' style="height:30px;width:70px">
</div>
</form>
<div class="col-10">
<span id="searchResult">Found {{ day_care_queryset.count }} results for your dog!</span>
<div class="cards">
{% for daycare in day_care_queryset %}
<div class="card">
<img src="{{ daycare.get_daycare_primary_image_url }}" alt="{{ daycare.name }} image" class="card-img-top">
<div class="card-body">
<div style="display: flex; flex-direction: row">
<h5 class="card-title">{{ daycare.name }}</h5>
</div>
<p class="card-text">{{ daycare.area | truncatechars:20 }}</p>
<p class="card-text">{{ daycare.city | truncatechars:20 }}</p>
<p class="card-text">{{ daycare.price_per_day | truncatechars:20 }}</p>
<p class="card-text">{{ daycare.description | truncatechars:35}}</p>
<a href="/daycare/{{ daycare.id }}" class="btn btn-warning">Daycare Profile</a>
</div>
</div>
{% endfor %}
</div>
</div>
<p class="card-text">{{ daycare.description | truncatechars:250 }}</p>
<a href="/daycare/{{ daycare.id }}" class="btn btn-primary">Daycare Profile</a>
</div>
</div>
{% endfor %}
</div>
</div>

{% endblock %}
{% endblock %}
18 changes: 17 additions & 1 deletion dogowner/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from daycare.models import DayCare
from daycare.forms import DayCareSearchForm
from orders.models import Order


@login_required()
def dog_owner_home(request):
form = DayCareSearchForm(request.POST or None)
day_care_queryset = DayCare.objects.all()
if request.method == 'POST':
if form.is_valid():
filter_day_cares = DayCare.objects.filter(area__startswith=form['area'].value(),
city__icontains=form['city'].value(),
name__icontains=form['name'].value(),
price_per_day__lte=form['price_per_day'].value())

available_day_cares = Order.get_all_day_cares_available_on_dates(form['start_date'].value(),
form['end_date'].value())
day_care_queryset = filter_day_cares.intersection(available_day_cares)

context = {
'daycares': DayCare.objects.all(),
'day_care_queryset': day_care_queryset,
'form': form
}
return render(request, 'dogowner/dog_owner_homepage.html', context)
1 change: 0 additions & 1 deletion main/templates/main/base_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
{% if request.user.is_authenticated %}
<li class="nav-item"><a class="nav-link text-white" href="/profile">Profile</a></li>
<li class="nav-item"><a class="nav-link text-white" href="">Orders</a></li>
<li class="nav-item"><a class="nav-link text-white" href="">Search</a></li>
<li class="nav-item"><a class="nav-link text-white" href="">Chats</a></li>
{% endif %}
<li class="nav-item"><a class="nav-link text-white" href="/about">About</a></li>
Expand Down
64 changes: 62 additions & 2 deletions main/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest
from dogowner.models import DogOwner
from daycare.models import DayCare
from orders.models import Order


@pytest.mark.django_db
Expand Down Expand Up @@ -72,8 +74,66 @@ def test_unlogged_user_access_to_homepage(self, client):
assert response.status_code == 302
assert response['Location'] == '/login/?next=/homepage/'

def test_dog_owner_homepage_is_visible_for_dog_owner(self, client, create_dog_owner_user):

@pytest.mark.django_db
class TestDogOwnerHomePageView:
def test_dog_owner_homepage_present_all_daycares(self, client, create_dog_owner_user):
client.force_login(user=create_dog_owner_user.user)
response = client.get("/homepage/")
assert response.status_code == 200
assert list(response.context['daycares']) == list(DayCare.objects.all())
assert set(response.context['day_care_queryset']) == set(DayCare.objects.all())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this test is ensuring that the page will have a list of all the day cares, maybe it should be mentioned in its name. or in a different test.

Copy link
Collaborator Author

@tamirmatok tamirmatok May 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NP , Fixed by changing the name to " test_dog_owner_homepage_present_all_daycares"


def test_daycare_not_appears_in_search_after_reduce_capacity_on_dates_to_0(self, client,
create_dog_owner_user,
create_daycare_user):
search_form = {'area': "",
'city': "",
'price_per_day': 100,
'name': "",
'start_date': "2022-05-03",
'end_date': "2022-05-08",
}
daycare_user = create_daycare_user
client.force_login(user=create_dog_owner_user.user)
response = client.post('/homepage/', search_form, follow=True)
day_care_queryset = response.context['day_care_queryset']
assert daycare_user in day_care_queryset
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make the test look nicer if the assertion would be separated with newline from the test setup logic (all over this file).


for _ in range(daycare_user.capacity):
Order.create(dog_owner_id=DogOwner.objects.get(id=1), daycare_id=daycare_user,
start_date="2022-05-02", end_date="2022-08-02", price_per_day=100).approve_order()
response = client.post('/homepage/', search_form, follow=True)
assert daycare_user not in response.context['day_care_queryset']

def test_successful_dog_owner_search_for_day_care(self, client, create_dog_owner_user):
client.force_login(user=create_dog_owner_user.user)
search_form = {'area': "C",
'city': "tel aviv",
'price_per_day': 800,
'name': "",
'start_date': "2022-05-03",
'end_date': "2022-05-08",
}
response = client.post('/homepage/', search_form, follow=True)
day_care_queryset = response.context['day_care_queryset']
available_day_cares = Order.get_all_day_cares_available_on_dates("2022-05-03", "2022-05-08")
filter_day_cares = DayCare.objects.filter(area__startswith='C',
city__icontains="tel aviv",
price_per_day__lte=800)
expected_queryset = available_day_cares.intersection(filter_day_cares)
assert set(day_care_queryset) == set(expected_queryset)

def test_search_for_day_care_with_start_date_greater_than_end_date_show_error_and_present_all_daycares(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By mentioning with_start_date_greater_than_end_date it makes me think this is the only case all daycares will show.
Would it make sense to rename this test to something like:
test_dog_owner_page_view_presents_all_daycares_when_search_results_is_empty?
In other words, would all daycare show in case of a search error?

self, client, create_dog_owner_user):
client.force_login(user=create_dog_owner_user.user)
search_form = {'area': "C",
'city': "tel aviv",
'price_per_day': 800,
'name': "",
'start_date': "2022-05-03",
'end_date': "2022-05-01",
}
response = client.post('/homepage/', search_form, follow=True)
day_care_queryset = response.context['day_care_queryset']
assert set(day_care_queryset) == set(DayCare.objects.all())
assert response.context['form']._errors['end_date']
37 changes: 37 additions & 0 deletions orders/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django.db import models
from django.db.models import QuerySet
from django.utils import timezone
from daycare.models import DayCare
from dogowner.models import DogOwner
import datetime


class StatusOptions(models.TextChoices):
Expand Down Expand Up @@ -71,3 +73,38 @@ def cancel_order(self):

def get_order_total_price(self):
return self.price_per_day * (self.end_date - self.start_date).days

@staticmethod
def get_capacity_of_daycare_in_dates_range(daycare_id, start_date, end_date):
Omeramsc marked this conversation as resolved.
Show resolved Hide resolved
relevant_orders = Order.objects.filter(daycare_id=daycare_id, status__in=['A', 'O'])
start_date = datetime.date(year=start_date.year, month=start_date.month, day=start_date.day)
Omeramsc marked this conversation as resolved.
Show resolved Hide resolved
end_date = datetime.date(year=end_date.year, month=end_date.month, day=end_date.day)
Omeramsc marked this conversation as resolved.
Show resolved Hide resolved
capacity_per_day_list = [0] * ((end_date - start_date).days + 2)

for order in relevant_orders:
if end_date < order.start_date or order.end_date < start_date:
continue

number_of_days = (order.end_date - order.start_date).days

for day in range(number_of_days):
current_date = order.start_date + datetime.timedelta(days=day)
if current_date < start_date:
continue
elif current_date > end_date:
break
else:
capacity_per_day_list[day] = capacity_per_day_list[day] + 1

return capacity_per_day_list

@staticmethod
def get_all_day_cares_available_on_dates(start_date: str, end_date: str) -> QuerySet:
start_date = datetime.date.fromisoformat(start_date)
Omeramsc marked this conversation as resolved.
Show resolved Hide resolved
end_date = datetime.date.fromisoformat(end_date)
id_list = []
for day_care in DayCare.objects.all():
capacity_per_day_list = Order.get_capacity_of_daycare_in_dates_range(day_care.id, start_date, end_date)
if all(current_capacity < day_care.capacity for current_capacity in capacity_per_day_list):
id_list.append(day_care.id)
return DayCare.objects.filter(id__in=id_list)
21 changes: 21 additions & 0 deletions orders/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,24 @@ def test_dog_owner_id_is_deleted_when_dog_owner_is_deleted(self, create_order):
def test_daycare_id_is_deleted_when_daycare_is_deleted(self, create_order):
DayCare.objects.get(id=1).delete()
assert Order.objects.get(id=create_order.id).daycare_id is None

@pytest.mark.parametrize('start_date, end_date',
[
("2022-05-02", "2022-08-02"),
])
def test_day_care_available_on_dates_appears_in_available_day_cares_queryset(self, create_daycare_user,
start_date, end_date):
day_care_user = create_daycare_user
assert day_care_user in Order.get_all_day_cares_available_on_dates(start_date, end_date)

@pytest.mark.parametrize('start_date, end_date',
[
("2022-05-02", "2022-08-02"),
])
def test_day_care_not_available_on_dates_not_appears_in_available_day_cares_queryset(self, create_daycare_user,
start_date, end_date):
day_care_user = create_daycare_user
for _ in range(day_care_user.capacity):
Order.create(dog_owner_id=DogOwner.objects.get(id=1), daycare_id=day_care_user,
start_date=start_date, end_date=end_date, price_per_day=500).approve_order()
assert day_care_user not in Order.get_all_day_cares_available_on_dates(start_date, end_date)
Loading