diff --git a/backend/sublet/migrations/0005_sublet_is_draft_alter_sublet_end_date_and_more.py b/backend/sublet/migrations/0005_sublet_is_draft_alter_sublet_end_date_and_more.py new file mode 100644 index 00000000..ef9d9068 --- /dev/null +++ b/backend/sublet/migrations/0005_sublet_is_draft_alter_sublet_end_date_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.2 on 2024-03-15 22:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sublet", "0004_alter_sublet_external_link"), + ] + + operations = [ + migrations.AddField( + model_name="sublet", name="is_draft", field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name="sublet", name="end_date", field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="sublet", + name="expires_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name="sublet", name="price", field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="sublet", name="start_date", field=models.DateField(blank=True, null=True), + ), + ] diff --git a/backend/sublet/migrations/0006_remove_sublet_is_draft_sublet_is_published.py b/backend/sublet/migrations/0006_remove_sublet_is_draft_sublet_is_published.py new file mode 100644 index 00000000..4e9b0bf5 --- /dev/null +++ b/backend/sublet/migrations/0006_remove_sublet_is_draft_sublet_is_published.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-03-22 21:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sublet", "0005_sublet_is_draft_alter_sublet_end_date_and_more"), + ] + + operations = [ + migrations.RemoveField(model_name="sublet", name="is_draft",), + migrations.AddField( + model_name="sublet", name="is_published", field=models.BooleanField(default=False), + ), + ] diff --git a/backend/sublet/models.py b/backend/sublet/models.py index e391053a..2364cbb6 100644 --- a/backend/sublet/models.py +++ b/backend/sublet/models.py @@ -33,6 +33,7 @@ class Sublet(models.Model): sublettees = models.ManyToManyField( User, through=Offer, related_name="sublets_offered", blank=True ) + is_published = models.BooleanField(default=False) favorites = models.ManyToManyField(User, related_name="sublets_favorited", blank=True) amenities = models.ManyToManyField(Amenity, blank=True) @@ -42,12 +43,12 @@ class Sublet(models.Model): baths = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True) description = models.TextField(null=True, blank=True) external_link = models.URLField(max_length=255, null=True, blank=True) - price = models.IntegerField() + price = models.IntegerField(null=True, blank=True) negotiable = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField() - start_date = models.DateField() - end_date = models.DateField() + expires_at = models.DateTimeField(null=True, blank=True) + start_date = models.DateField(null=True, blank=True) + end_date = models.DateField(null=True, blank=True) def __str__(self): return f"{self.title} by {self.subletter}" diff --git a/backend/sublet/serializers.py b/backend/sublet/serializers.py index 549e9e2b..30d2e949 100644 --- a/backend/sublet/serializers.py +++ b/backend/sublet/serializers.py @@ -61,6 +61,36 @@ class SubletSerializer(serializers.ModelSerializer): many=True, queryset=Amenity.objects.all(), required=False ) + def validate_publish(self, validated_data, instance=None): + fields = [ + "title", + "address", + "price", + "negotiable", + "start_date", + "end_date", + "expires_at", + ] + + newest_fields = [ + ( + field, + ( + validated_data[field] + if field in validated_data + else getattr(instance, field, None) + if instance + else None + ), + ) + for field in fields + ] + + if bad_fields := [field[0] for field in newest_fields if not field[1]]: + raise serializers.ValidationError( + f"fields: {', '.join(bad_fields)} are required to publish sublet." + ) + class Meta: model = Sublet read_only_fields = [ @@ -72,7 +102,7 @@ class Meta: ] fields = [ "id", - "subletter", + "is_published", "amenities", "title", "address", @@ -93,6 +123,8 @@ class Meta: def create(self, validated_data): validated_data["subletter"] = self.context["request"].user + if validated_data["is_published"]: + self.validate_publish(validated_data) instance = super().create(validated_data) instance.save() return instance @@ -104,6 +136,10 @@ def update(self, instance, validated_data): self.context["request"].user == instance.subletter or self.context["request"].user.is_superuser ): + if ("is_published" in validated_data and validated_data["is_published"]) or ( + "is_published" not in validated_data and instance.is_published + ): + self.validate_publish(validated_data, instance) instance = super().update(instance, validated_data) instance.save() return instance @@ -133,6 +169,8 @@ class Meta: fields = [ "id", "subletter", + "sublettees", + "is_published", "amenities", "title", "address", @@ -142,6 +180,7 @@ class Meta: "external_link", "price", "negotiable", + "created_at", "start_date", "end_date", "expires_at", @@ -160,6 +199,7 @@ class Meta: model = Sublet fields = [ "id", + "is_published", "subletter", "amenities", "title", diff --git a/backend/sublet/views.py b/backend/sublet/views.py index 38623631..4545fb14 100644 --- a/backend/sublet/views.py +++ b/backend/sublet/views.py @@ -148,6 +148,7 @@ def list(self, request, *args, **kwargs): queryset = queryset.filter(subletter=request.user) else: queryset = queryset.filter(expires_at__gte=timezone.now()) + queryset = queryset.filter(is_published=True) if title: queryset = queryset.filter(title__icontains=title) if address: diff --git a/backend/tests/portal/test_polls.py b/backend/tests/portal/test_polls.py index 71c1a33c..23995ffd 100644 --- a/backend/tests/portal/test_polls.py +++ b/backend/tests/portal/test_polls.py @@ -97,7 +97,7 @@ def setUp(self): poll.save() poll_1 = Poll.objects.get(question="How is your day") - self.id = poll_1.id + self.poll_id = poll_1.id @mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs) def test_create_poll(self): @@ -122,11 +122,11 @@ def test_update_poll(self): payload = { "question": "New question", } - response = self.client.patch(f"/portal/polls/{self.id}/", payload) + response = self.client.patch(f"/portal/polls/{self.poll_id}/", payload) res_json = json.loads(response.content) # asserts that the update worked - self.assertEqual(self.id, res_json["id"]) - self.assertEqual("New question", Poll.objects.get(id=self.id).question) + self.assertEqual(self.poll_id, res_json["id"]) + self.assertEqual("New question", Poll.objects.get(id=self.poll_id).question) @mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs) @mock.patch("portal.logic.get_user_info", mock_get_user_info) @@ -158,14 +158,14 @@ def test_null_user_info_browse(self): @mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs) @mock.patch("portal.logic.get_user_info", mock_get_user_info) def test_create_option(self): - payload_1 = {"poll": self.id, "choice": "yes!"} - payload_2 = {"poll": self.id, "choice": "no!"} + payload_1 = {"poll": self.poll_id, "choice": "yes!"} + payload_2 = {"poll": self.poll_id, "choice": "no!"} self.client.post("/portal/options/", payload_1) self.client.post("/portal/options/", payload_2) self.assertEqual(2, PollOption.objects.all().count()) # asserts options were created and were placed to right poll for poll_option in PollOption.objects.all(): - self.assertEqual(Poll.objects.get(id=self.id), poll_option.poll) + self.assertEqual(Poll.objects.get(id=self.poll_id), poll_option.poll) response = self.client.post("/portal/polls/browse/", {"id_hash": 1}) res_json = json.loads(response.content) self.assertEqual(2, len(res_json[0]["options"])) @@ -173,11 +173,11 @@ def test_create_option(self): @mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs) @mock.patch("portal.views.get_user_clubs", mock_get_user_clubs) def test_update_option(self): - payload_1 = {"poll": self.id, "choice": "yes!"} + payload_1 = {"poll": self.poll_id, "choice": "yes!"} response = self.client.post("/portal/options/", payload_1) res_json = json.loads(response.content) self.assertEqual("yes!", PollOption.objects.get(id=res_json["id"]).choice) - payload_2 = {"poll": self.id, "choice": "no!"} + payload_2 = {"poll": self.poll_id, "choice": "no!"} # checks that poll's option was changed self.client.patch(f'/portal/options/{res_json["id"]}/', payload_2) self.assertEqual("no!", PollOption.objects.get(id=res_json["id"]).choice) @@ -200,11 +200,11 @@ def test_review_poll(self): @mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs) @mock.patch("portal.logic.get_user_info", mock_get_user_info) def test_more_than_five_options(self): - payload_1 = {"poll": self.id, "choice": "1"} - payload_2 = {"poll": self.id, "choice": "2"} - payload_3 = {"poll": self.id, "choice": "3"} - payload_4 = {"poll": self.id, "choice": "4"} - payload_5 = {"poll": self.id, "choice": "5"} + payload_1 = {"poll": self.poll_id, "choice": "1"} + payload_2 = {"poll": self.poll_id, "choice": "2"} + payload_3 = {"poll": self.poll_id, "choice": "3"} + payload_4 = {"poll": self.poll_id, "choice": "4"} + payload_5 = {"poll": self.poll_id, "choice": "5"} self.client.post("/portal/options/", payload_1) self.client.post("/portal/options/", payload_2) self.client.post("/portal/options/", payload_3) @@ -213,17 +213,17 @@ def test_more_than_five_options(self): self.assertEqual(5, PollOption.objects.all().count()) # asserts options were created and were placed to right poll for poll_option in PollOption.objects.all(): - self.assertEqual(Poll.objects.get(id=self.id), poll_option.poll) + self.assertEqual(Poll.objects.get(id=self.poll_id), poll_option.poll) response = self.client.post("/portal/polls/browse/", {"id_hash": 1}) res_json = json.loads(response.content) self.assertEqual(5, len(res_json[0]["options"])) # adding more than 5 options to same poll should not be allowed - payload_6 = {"poll": self.id, "choice": "6"} + payload_6 = {"poll": self.poll_id, "choice": "6"} response = self.client.post("/portal/options/", payload_6) self.assertEqual(5, PollOption.objects.all().count()) def test_option_vote_view(self): - response = self.client.get(f"/portal/polls/{self.id}/option_view/") + response = self.client.get(f"/portal/polls/{self.poll_id}/option_view/") res_json = json.loads(response.content) self.assertEqual("pennlabs", res_json["club_code"]) # test that options key is in response diff --git a/backend/tests/portal/test_posts.py b/backend/tests/portal/test_posts.py index a4dd9d5a..ef3f9d48 100644 --- a/backend/tests/portal/test_posts.py +++ b/backend/tests/portal/test_posts.py @@ -58,7 +58,7 @@ def setUp(self): post_1 = Post.objects.all().first() post_1.status = Post.STATUS_APPROVED post_1.save() - self.id = post_1.id + self.post_id = post_1.id @mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs) def test_create_post(self): @@ -100,10 +100,10 @@ def test_fail_post(self): @mock.patch("portal.permissions.get_user_clubs", mock_get_user_clubs) def test_update_post(self): payload = {"title": "New Test Title 3"} - response = self.client.patch(f"/portal/posts/{self.id}/", payload) + response = self.client.patch(f"/portal/posts/{self.post_id}/", payload) res_json = json.loads(response.content) - self.assertEqual(self.id, res_json["id"]) - self.assertEqual("New Test Title 3", Post.objects.get(id=self.id).title) + self.assertEqual(self.post_id, res_json["id"]) + self.assertEqual("New Test Title 3", Post.objects.get(id=self.post_id).title) # since the user is not an admin, approved should be set to false after update self.assertEqual(Post.STATUS_DRAFT, res_json["status"]) @@ -113,9 +113,9 @@ def test_update_post_admin(self): admin = User.objects.create_superuser("admin@upenn.edu", "admin", "admin") self.client.force_authenticate(user=admin) payload = {"title": "New Test Title 3"} - response = self.client.patch(f"/portal/posts/{self.id}/", payload) + response = self.client.patch(f"/portal/posts/{self.post_id}/", payload) res_json = json.loads(response.content) - self.assertEqual(self.id, res_json["id"]) + self.assertEqual(self.post_id, res_json["id"]) self.assertEqual(Post.STATUS_APPROVED, res_json["status"]) @mock.patch("portal.serializers.get_user_clubs", mock_get_user_clubs) diff --git a/backend/tests/sublet/test_sublets.py b/backend/tests/sublet/test_sublets.py index 38199c85..4556672a 100644 --- a/backend/tests/sublet/test_sublets.py +++ b/backend/tests/sublet/test_sublets.py @@ -49,6 +49,7 @@ def test_create_sublet(self): "start_date": "3000-04-09", "end_date": "3000-08-07", "amenities": ["Amenity1", "Amenity2"], + "is_published": True, } response = self.client.post("/sublet/properties/", payload) res_json = json.loads(response.content) @@ -65,6 +66,7 @@ def test_create_sublet(self): "start_date", "end_date", "amenities", + "is_published", ] [self.assertEqual(payload[key], res_json[key]) for key in match_keys] self.assertIn("id", res_json) @@ -91,7 +93,7 @@ def test_update_sublet(self): response = self.client.post("/sublet/properties/", payload) old_id = json.loads(response.content)["id"] # Update the sublet using the serializer - data = {"title": "New Title", "beds": 3, "amenities": ["Amenity1"]} + data = {"title": "New Title", "beds": 3, "amenities": ["Amenity1"], "address": ""} response = self.client.patch(f"/sublet/properties/{str(old_id)}/", data) res_json = json.loads(response.content) self.assertEqual(3, res_json["beds"]) @@ -99,6 +101,15 @@ def test_update_sublet(self): self.assertEqual("New Title", Sublet.objects.get(id=old_id).title) self.assertEqual("New Title", res_json["title"]) self.assertEqual(1, len(res_json["amenities"])) + payload["address"] = "" + payload["is_published"] = True + response = self.client.patch(f"/sublet/properties/{str(old_id)}/", payload) + self.assertEqual(400, response.status_code) + payload["is_published"] = False + response = self.client.patch(f"/sublet/properties/{str(old_id)}/", payload) + self.assertEqual(200, response.status_code) + response = self.client.patch(f"/sublet/properties/{str(old_id)}/", {"is_published": True}) + self.assertEqual(400, response.status_code) def test_browse_sublets(self): response = self.client.get("/sublet/properties/") @@ -117,6 +128,7 @@ def test_browse_sublets(self): "start_date": "3000-04-09", "end_date": "3000-08-07", "amenities": ["Amenity1", "Amenity2"], + "is_published": True, } response = self.client.post("/sublet/properties/", payload) old_id = json.loads(response.content)["id"] @@ -143,6 +155,7 @@ def test_browse_filtered(self): "start_date": "3000-04-09", "end_date": "3000-08-07", "amenities": ["Amenity1", "Amenity2"], + "is_published": True, } response = self.client.post("/sublet/properties/", payload) old_id = json.loads(response.content)["id"] diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 00000000..e69de29b