diff --git a/conftest.py b/conftest.py index 39d2db9..86aedf9 100644 --- a/conftest.py +++ b/conftest.py @@ -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" pytest.CITY = "Tel-Aviv" pytest.ADDRESS = "The best street 5" diff --git a/daycare/forms.py b/daycare/forms.py new file mode 100644 index 0000000..3e8d3d0 --- /dev/null +++ b/daycare/forms.py @@ -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') + 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 diff --git a/daycare/migrations/0006_alter_daycare_area.py b/daycare/migrations/0006_alter_daycare_area.py new file mode 100644 index 0000000..73bd90d --- /dev/null +++ b/daycare/migrations/0006_alter_daycare_area.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.3 on 2022-04-27 13:37 + +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]), + ), + ] diff --git a/daycare/models.py b/daycare/models.py index 54aa6ea..6911299 100644 --- a/daycare/models.py +++ b/daycare/models.py @@ -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) city = models.CharField(max_length=20, blank=True, validators=[MaxLengthValidator]) address = models.CharField(max_length=50, blank=True, validators=[MaxLengthValidator]) diff --git a/dogowner/templates/dogowner/dog_owner_homepage.html b/dogowner/templates/dogowner/dog_owner_homepage.html index 8434a05..cb6ff3d 100644 --- a/dogowner/templates/dogowner/dog_owner_homepage.html +++ b/dogowner/templates/dogowner/dog_owner_homepage.html @@ -1,4 +1,5 @@ {% extends "main/base_template.html" %} +{% load crispy_forms_tags %} {% load static %} {% block stylesheets %} @@ -6,21 +7,51 @@ {% endblock %} {% block content %} -
-
- {% for daycare in daycares %} -
- {{ daycare.name }} image -
-
-
{{ daycare.name }}
+ +
+
{% csrf_token %} +
+ Search for daycare: +
+ {{ form.start_date|as_crispy_field }} +
+
+ {{ form.end_date|as_crispy_field }} +
+
+ {{ form.price_per_day|as_crispy_field }} +
+
+ {{ form.area|as_crispy_field }} +
+
+ {{ form.city|as_crispy_field }} +
+
+ {{ form.name|as_crispy_field }} +
+ +
+
+
+ Found {{ day_care_queryset.count }} results for your dog! +
+ {% for daycare in day_care_queryset %} +
+ {{ daycare.name }} image +
+
+
{{ daycare.name }}
+
+

{{ daycare.area | truncatechars:20 }}

+

{{ daycare.city | truncatechars:20 }}

+

{{ daycare.price_per_day | truncatechars:20 }}

+

{{ daycare.description | truncatechars:35}}

+ Daycare Profile +
+
+ {% endfor %} +
-

{{ daycare.description | truncatechars:250 }}

- Daycare Profile -
- {% endfor %} -
-
- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/dogowner/views.py b/dogowner/views.py index 53333ed..7fc3285 100644 --- a/dogowner/views.py +++ b/dogowner/views.py @@ -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) diff --git a/main/templates/main/base_template.html b/main/templates/main/base_template.html index b045033..87360af 100644 --- a/main/templates/main/base_template.html +++ b/main/templates/main/base_template.html @@ -26,7 +26,6 @@ {% if request.user.is_authenticated %} - {% endif %} diff --git a/main/tests.py b/main/tests.py index 3d3107c..8a2547a 100644 --- a/main/tests.py +++ b/main/tests.py @@ -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 @@ -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()) + + 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 + + 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( + 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'] diff --git a/orders/models.py b/orders/models.py index 105e1f3..9c7ccb4 100644 --- a/orders/models.py +++ b/orders/models.py @@ -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): @@ -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): + 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) + end_date = datetime.date(year=end_date.year, month=end_date.month, day=end_date.day) + 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) + 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) diff --git a/orders/tests.py b/orders/tests.py index e9259ab..ac72c9c 100644 --- a/orders/tests.py +++ b/orders/tests.py @@ -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) diff --git a/static/CSS/dog_owner_homepage.css b/static/CSS/dog_owner_homepage.css index 7c77f17..092071b 100644 --- a/static/CSS/dog_owner_homepage.css +++ b/static/CSS/dog_owner_homepage.css @@ -1,32 +1,24 @@ .cards { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin-left: 50px; - justify-content: center; - align-items: center; + float:left; + margin: 0 0 0 5em; + position:relative; + width:100%; } -.row -{ - height: 30rem; - width: 120rem; - margin-left: 200px; +#searchResult { + font-weight:bold; + position: relative; + left: 6.3em; + top:30px; } .card { - width: 18.5%; + width: 330px; height: 30rem; - margin: 60px 60px 0 60px; - max-width: 100%; -} - -.card-body -{ - display: flex; - flex-direction: column; + float:left; + margin: 60px 20px 0 20px; } .card-body .btn @@ -38,4 +30,31 @@ .card-img-top { height: 200px; -} \ No newline at end of file +} + +#searchBoxArea { + background-color:#dcdcdc; + left:5em; + top: 5.2em; + max-width:100%; + padding: 2em 2em 2em 2em; +} + +#searchBoxArea span { + font-weight:bold; + font-size:18px; + color:#3e3e3e; + padding: 5px 0 1em 0; + float:left; +} + +#searchBoxArea input[type="submit"] { + background-color:#3e3e3e; + border: 0; + border-radius:12px; + color:#FFF; +} + +#searchBoxArea input[type="submit"]:hover { + background-color:#1e1e1e; +}