Skip to content
Snippets Groups Projects
Commit 62337437 authored by mdmuradalahi's avatar mdmuradalahi
Browse files

Merging

parents 2d54af51 158293c2
No related branches found
No related tags found
1 merge request!1062.2_BDD_TDD
Pipeline #47076 passed
Showing
with 469 additions and 30 deletions
......@@ -37,5 +37,5 @@ config :phoenix_live_view,
enable_expensive_runtime_checks: true
# Hound Configuration
config :hound, driver: "chrome_driver", port: 57429 # update the port according to your local chrome_drive message
config :hound, driver: "chrome_driver", port: 58968 # update the port according to your local chrome_drive message
config :proptracker, sql_sandbox: true
Feature: Manage Advertisement Interests
Scenario: Adding an advertisement to interests
Given a logged-in user is viewing an advertisement
When the user clicks the "Add to Interests" button
Then the advertisement should be added to the user's interests list
Scenario: Removing an advertisement from interests
Given a logged-in user has already added an advertisement to their interests list
When the user clicks the "Remove from Interests" button
Then the advertisement should be removed from the user's interests list
......@@ -76,4 +76,8 @@ defmodule WhiteBreadConfig do
suite name: "3.11_owner_profile_Suite",
context: ProptrackerWeb.ViewOwnerProfileContext,
feature_paths: ["features/3.11_owner_profile.feature"]
suite name: "3.4_interest_Suite",
context: ProptrackerWeb.InterestsContext,
feature_paths: ["features/3.4_interest.feature"]
end
defmodule ProptrackerWeb.InterestsContext do
use WhiteBread.Context
alias Proptracker.Repo
alias Proptracker.Accounts.{User, Advertisement}
import Phoenix.ConnTest, only: [build_conn: 0, post: 3]
@moduledoc """
Context for testing advertisement interests functionality.
"""
# Given a registered user
given_ ~r/^a registered user$/ do
fn state ->
user_data = %{username: "testuser", password: "password123", password_confirmation: "password123"}
changeset = User.registration_changeset(%User{}, user_data)
{:ok, user} = Repo.insert(changeset)
{:ok, Map.put(state, :user, user)}
end
end
# Given a logged-in user is viewing an advertisement
given_ ~r/^a logged-in user is viewing an advertisement$/ do
fn state ->
user = state[:user]
# Create an advertisement
advertisement = %Advertisement{
title: "Test Advertisement",
price: 1000,
location: "Test Location",
description: "Test Description"
}
# Insert the advertisement into the database
{:ok, advertisement} = Repo.insert(advertisement)
{:ok, Map.put(state, :advertisement, advertisement)}
end
end
# When the user logs in and the session is set
when_ ~r/^the user logs in$/ do
fn state ->
user = state[:user]
conn = build_conn()
# Simulate the session by adding the user to the connection
conn = %{conn | assigns: %{user_id: user.id}}
{:ok, Map.put(state, :conn, conn)}
end
end
# When the user clicks the "Add to Interests" button
when_ ~r/^the user clicks the "(?<argument_one>[^"]+)" button$/ do
fn state ->
conn = state[:conn]
advertisement_id = state[:advertisement].id
# Simulate the button click and make the POST request to toggle the interest status
conn = post(conn, "/advertisements/#{advertisement_id}/toggle_interest", %{})
{:ok, Map.put(state, :conn, conn)}
end
end
# Then the advertisement should be added to the user's interests list
then_ ~r/^the advertisement should be added to the user's interests list$/ do
fn state ->
user = state[:user]
advertisement = state[:advertisement]
# Check if the advertisement is in the user's interests list
updated_user = Repo.get(User, user.id)
assert advertisement.id in updated_user.interested_advertisements
{:ok, state}
end
end
# Then the advertisement should be removed from the user's interests list
then_ ~r/^the advertisement should be removed from the user's interests list$/ do
fn state ->
user = state[:user]
advertisement = state[:advertisement]
# Check if the advertisement has been removed from the user's interests list
updated_user = Repo.get(User, user.id)
refute advertisement.id in updated_user.interested_advertisements
{:ok, state}
end
end
# Given a logged-in user has already added an advertisement to their interests list
given_ ~r/^a logged-in user has already added an advertisement to their interests list$/ do
fn state ->
user = state[:user]
advertisement = state[:advertisement]
# Add the advertisement to the user's interests list
updated_user = Repo.get(User, user.id)
updated_interests = [advertisement.id | updated_user.interested_advertisements]
# Update the user's interests list in the database
changeset = Ecto.Changeset.change(updated_user, %{interested_advertisements: updated_interests})
Repo.update!(changeset)
{:ok, Map.put(state, :user, updated_user)}
end
end
end
defmodule ProptrackerWeb.Features.Contexts.HomepageContext do
defmodule ProptrackerWeb.Features.Contexts.SoldRent do
use WhiteBread.Context
import Plug.Conn
import Phoenix.ConnTest
......
......@@ -16,6 +16,8 @@ defmodule Proptracker.Accounts.User do
field :interested, {:array, :integer}, default: []
field :not_interested, {:array, :integer}, default: []
has_many :advertisements, Proptracker.Accounts.Advertisement
timestamps()
end
......@@ -42,6 +44,7 @@ defmodule Proptracker.Accounts.User do
|> validate_username_uniqueness()
|> validate_phone_number()
|> validate_date_of_birth()
|> validate_password()
end
defp validate_username_uniqueness(changeset) do
......@@ -71,13 +74,15 @@ defmodule Proptracker.Accounts.User do
defp validate_phone_number(changeset) do
phone_number = get_field(changeset, :phone_number)
if phone_number && String.to_integer(phone_number) < 0 do
add_error(changeset, :phone_number, "must be a positive number")
# Check if the phone number is a valid positive number with at least 6 digits
if phone_number && (String.length(phone_number) < 6 or String.to_integer(phone_number) < 0) do
add_error(changeset, :phone_number, "must be a positive number with at least 6 digits")
else
changeset
end
end
defp validate_date_of_birth(changeset) do
date_of_birth = get_field(changeset, :date_of_birth)
......@@ -87,4 +92,15 @@ defmodule Proptracker.Accounts.User do
changeset
end
end
defp validate_password(changeset) do
password = get_field(changeset, :password)
# Check if the password is at least 6 characters long
if password && String.length(password) < 6 do
add_error(changeset, :password, "must be at least 6 characters long")
else
changeset
end
end
end
......@@ -178,7 +178,7 @@ defmodule ProptrackerWeb.AdvertisementController do
all_advertisements = Repo.all(Advertisement)
recommended_properties = Advertisement.get_similar_advertisements(advertisement, all_advertisements)
current_user = if user_id, do: Repo.get(Proptracker.Accounts.User, user_id), else: nil
current_user = if user_id, do: Repo.get(Proptracker.Accounts.User, advertisement.user_id), else: nil
render(conn, "show.html",
advertisement: advertisement,
recommended_properties: recommended_properties,
......
<head>
<meta property="og:title" content={@advertisement.title} />
<meta property="og:description" content={@advertisement.description} />
<meta property="og:url" content={"http://localhost:4000/advertisements/#{@advertisement.id}"} />
<meta property="og:image" content={List.first(@advertisement.pictures) || "/images/default_image.jpg"} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Prop Tracker Group 4" />
</head>
<style>
/* Overall layout */
.details-container {
......@@ -104,9 +113,8 @@
/* Contact Button */
.contact-button {
background-color: #007bff;
color: white;
padding: 10px 20px;
color: #e37303;
padding: 12px 24px;
font-size: 1rem;
border: none;
border-radius: 25px;
......@@ -116,16 +124,57 @@
}
.contact-button:hover {
background-color: #0056b3;
color: #aa5e12;
}
/* Contact Info */
.contact-info {
background-color: #fff0e3;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
margin-top: 10px;
font-size: 1.2rem;
color: #333;
display: none; /* Hidden by default */
}
/* Facebook Share Button Style */
.social-button.facebook {
background-color: #1877F2; /* Facebook blue */
color: white;
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
border: none;
border-radius: 30px;
cursor: pointer;
transition: background-color 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Icon style (optional Facebook icon) */
.social-button.facebook:before { /* Facebook icon */
margin-right: 10px;
width: 18px;
height: 18px;
}
/* Hover effect */
.social-button.facebook:hover {
background-color: #1668c7; /* Darker blue on hover */
}
/* Focus and active states */
.social-button.facebook:focus, .social-button.facebook:active {
outline: none;
background-color: #145dbf; /* Even darker blue when clicked */
}
</style>
<div class="details-container">
......@@ -146,13 +195,24 @@
<%= String.capitalize(@advertisement.state) %>
</span></p>
<p><strong>Reference:</strong> <%= @advertisement.reference %></p>
<a href="https://www.facebook.com/sharer/sharer.php?u=http://localhost:4000/advertisements/{@advertisement.id}" target="_blank">
<button class="social-button facebook">Share on Facebook</button>
</a>
<!-- Contact Button -->
<button class="contact-button" onclick="toggleContactInfo()">Contact</button>
<button class="contact-button" onclick="toggleContactInfo()">+ Owner Contact Info</button>
<!-- Owner's Phone Number (Initially hidden) -->
<div class="contact-info" id="contact-info">
<strong>Owner's Phone:</strong> <%= @current_user.phone_number %>
<div class="contact-info" id="contact-info"
style = "
display: hidden;
padding: 15px;
border-radius: 15px;
"
>
<strong>Owner's Phone:</strong> <%= @current_user.phone_number %>
<br>
<strong>Owner's Name:</strong> <%= @current_user.name %>
</div>
</div>
......@@ -178,7 +238,7 @@
<p><strong>Rooms:</strong> <%= property.advertisement.rooms %></p>
<p><strong>Surface Area:</strong> <%= property.advertisement.square_meters %></p>
<p><strong>Location:</strong> <%= property.advertisement.location %></p>
</div>
</div>
</.link>
<% end %>
</div>
......
......@@ -13,28 +13,68 @@ defmodule ProptrackerWeb.PageController do
# Retrieve the current user if logged in
current_user = if user_id, do: Repo.get(Proptracker.Accounts.User, user_id), else: nil
# Fetch all advertisements, prioritizing available ones
# Retrieve the list of IDs of advertisements that the user is "interested" in
interested_ads = if current_user, do: current_user.interested, else: []
# Fetch advertisements excluding those marked as sold/rented
advertisements =
Advertisement
|> where([a], a.state != "sold/rented")
|> where([a], a.state != "reserved")
|> Repo.all()
Advertisement
|> where([a], a.state != "sold/rented")
|> Repo.all()
# Get available properties that the user is NOT interested in (i.e., not in the interested list)
available_ads_not_interested =
Advertisement
|> where([a], a.state == "available" and not a.id in ^interested_ads)
|> order_by([a], asc: a.state)
|> Repo.all()
# Get reserved properties that the user is NOT interested in
reserved_ads_not_interested =
Advertisement
|> where([a], a.state == "reserved" and not a.id in ^interested_ads)
|> order_by([a], asc: a.state)
|> Repo.all()
# Get available properties that the user is interested in
available_ads_interested =
Advertisement
|> where([a], a.state == "available" and a.id in ^interested_ads)
|> order_by([a], asc: a.state)
|> Repo.all()
# Get reserved properties that the user is interested in
reserved_ads_interested =
Advertisement
|> where([a], a.state == "reserved" and a.id in ^interested_ads)
|> order_by([a], asc: a.state)
|> Repo.all()
# Combine the results to return in the desired order
advertisements =
available_ads_not_interested ++
reserved_ads_not_interested ++
available_ads_interested ++
reserved_ads_interested
# Render the page with advertisements and current_user
render(conn, "home1.html",
advertisements: advertisements,
current_user: current_user,
type: nil, # Default to nil
type: nil,
state: nil,
location: "",
query: "", # Default to empty string
query: "",
min_price: "",
max_price: "",
min_rooms: "",
max_rooms: "",
layout: false
)
)
end
@spec team(Plug.Conn.t(), any()) :: Plug.Conn.t()
def team(conn, _params) do
render(conn, "team.html",
......
......@@ -72,6 +72,7 @@ defmodule ProptrackerWeb.SearchController do
|> (fn q -> if max_price != nil, do: from(a in q, where: a.price <= ^max_price), else: q end).()
|> (fn q -> if min_rooms != nil, do: from(a in q, where: a.rooms >= ^min_rooms), else: q end).()
|> (fn q -> if max_rooms != nil, do: from(a in q, where: a.rooms <= ^max_rooms), else: q end).()
|> where([a], a.state != "sold/rented")
|> order_by([a], asc: a.state) # Add ordering by 'state', available first
|> Repo.all()
......
......@@ -51,9 +51,15 @@ defmodule ProptrackerWeb.SessionController do
def logout(conn, _params) do
conn
|> configure_session(drop: true)
|> put_flash(:info, "Logged out successfully.")
|> redirect(to: "/login")
if get_session(conn, :user_id) do
conn
|> configure_session(drop: true)
|> put_flash(:info, "Logged out successfully.")
|> redirect(to: "/login")
else
conn
|> put_flash(:error, "No active session to log out from.")
|> redirect(to: "/login")
end
end
end
......@@ -5,6 +5,7 @@ defmodule ProptrackerWeb.UserController do
alias Proptracker.Repo
alias Proptracker.Accounts.User
alias Proptracker.Accounts.Advertisement
import Ecto.Query
def index(conn, _params) do
users = Repo.all(User)
......@@ -74,14 +75,23 @@ defmodule ProptrackerWeb.UserController do
def delete(conn, %{"id" => id}) do
user = Repo.get!(User, id)
Repo.delete!(user)
user = Repo.get(User, id)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: ~p"/login")
# Check if the current user is allowed to delete the user
if conn.assigns[:current_user].id == user.id or conn.assigns[:current_user].role == "admin" do
Repo.delete!(user)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: ~p"/login")
else
# If not authorized, redirect to users page with error message
conn
|> put_flash(:error, "You do not have permission to delete this user.")
|> redirect(to: ~p"/users") # Redirect to users page instead of login
end
end
def owner_profile(conn, %{"id" => advertisement_id}) do
# Fetch the advertisement by ID
advertisement = Advertisement.get_advertisement!(advertisement_id)
......
......@@ -27,5 +27,17 @@ defmodule ProptrackerWeb.SessionControllerTest do
# Verify redirect and flash message
assert redirected_to(conn) == ~p"/login"
end
test "Fails to logout when session is missing user_id", %{conn: conn} do
# Simulate a connection with an empty session
conn = build_conn() |> init_test_session(%{})
# Attempt to log out
conn = delete(conn, "/logout")
# Verify the response handles the missing session gracefully
assert redirected_to(conn) == ~p"/login"
assert get_flash(conn, :error) == "No active session to log out from."
end
end
end
......@@ -21,4 +21,24 @@ defmodule ProptrackerWeb.UserControllerTest do
assert redirected_to(conn) == ~p"/login" # Manual path for index page
end
test "fails to delete user when user clicks on delete but lacks permissions", %{conn: conn, user: user} do
# Create an unauthorized user trying to delete another user
unauthorized_user = %User{id: 2, username: "unauthorized_user", password: "password"} |> Repo.insert!()
# Assign unauthorized user to the current session
conn = build_conn() |> assign(:current_user, unauthorized_user)
# Attempt to delete the user
delete_path = "/users/#{user.id}" # Path to delete user
conn = delete(conn, delete_path)
# Assert the response is a redirect (302)
assert conn.status == 302
# Ensure the user is redirected to the users page
assert redirected_to(conn) == "/login"
end
end
defmodule ProptrackerWeb.AdvertisementOrderTest do
use ProptrackerWeb.ConnCase
alias Proptracker.Accounts.{User, Advertisement}
alias Proptracker.Repo
@valid_user_attrs %{
name: "Test User",
username: "test_username",
password: "password123",
date_of_birth: ~D[1990-01-01],
phone_number: "987654321"
}
@valid_advertisement_attrs %{
title: "Available Ad",
pictures: ["path/to/image1.jpg", "path/to/image2.jpg"],
description: "Available Advertisement",
type: "rent",
price: Decimal.new("1000.00"),
square_meters: 100,
location: "City 1",
rooms: 2,
floor: 1,
total_floors: 5,
state: "available"
}
@reserved_advertisement_attrs %{
title: "Reserved Ad",
pictures: ["path/to/image3.jpg"],
description: "Reserved Advertisement",
type: "rent",
price: Decimal.new("1500.00"),
square_meters: 80,
location: "City 2",
rooms: 3,
floor: 2,
total_floors: 5,
state: "reserved"
}
@sold_advertisement_attrs %{
title: "Sold Ad",
pictures: ["path/to/image4.jpg"],
description: "Sold Advertisement",
type: "rent",
price: Decimal.new("2000.00"),
square_meters: 120,
location: "City 3",
rooms: 4,
floor: 3,
total_floors: 5,
state: "sold/rented"
}
# Create user helper function
defp create_user(_) do
{:ok, user} = Repo.insert(%User{} |> User.changeset(@valid_user_attrs))
%{user: user}
end
# Create advertisement helper function, preload user association
defp create_advertisements(%{user: user}) do
# Create available, reserved, and sold advertisements
available_attrs = Map.put(@valid_advertisement_attrs, :user_id, user.id)
{:ok, available_ad} = Repo.insert(Advertisement.changeset(%Advertisement{}, available_attrs))
reserved_attrs = Map.put(@reserved_advertisement_attrs, :user_id, user.id)
{:ok, reserved_ad} = Repo.insert(Advertisement.changeset(%Advertisement{}, reserved_attrs))
sold_attrs = Map.put(@sold_advertisement_attrs, :user_id, user.id)
{:ok, sold_ad} = Repo.insert(Advertisement.changeset(%Advertisement{}, sold_attrs))
%{available_ad: available_ad, reserved_ad: reserved_ad, sold_ad: sold_ad, user: user}
end
# Login helper function
defp login_user(conn, user) do
post(conn, "/login", user: %{username: user.username, password: user.password})
end
describe "advertisement order" do
setup [:create_user, :create_advertisements]
test "shows advertisements in correct order (available -> reserved -> sold)",
%{conn: conn, available_ad: available_ad, reserved_ad: reserved_ad, sold_ad: sold_ad, user: user} do
# Simulate logging in the user
conn = login_user(conn, user)
# Ensure user is logged in (session contains user_id)
assert get_session(conn, :user_id) == user.id
# Simulate a GET request to the advertisements page
conn = get(conn, ~p"/advertisements")
# Check the response
response = html_response(conn, 200)
# Parse the response with Floki to extract ad titles
ad_titles = Floki.find(response, ".advertisement-box .advertisement-title")
|> Enum.map(&Floki.text(&1)) # Extract the text of each title
# Ensure all ads are in the response
assert available_ad.title in ad_titles
assert reserved_ad.title in ad_titles
assert sold_ad.title in ad_titles
# Ensure the advertisements are listed in the correct order (available -> reserved -> sold)
available_ad_index = Enum.find_index(ad_titles, fn title -> title == available_ad.title end)
reserved_ad_index = Enum.find_index(ad_titles, fn title -> title == reserved_ad.title end)
sold_ad_index = Enum.find_index(ad_titles, fn title -> title == sold_ad.title end)
assert available_ad_index < reserved_ad_index
assert reserved_ad_index < sold_ad_index
end
test "ensures ads do not appear in the incorrect order (reserved -> available -> sold)",
%{conn: conn, available_ad: available_ad, reserved_ad: reserved_ad, sold_ad: sold_ad, user: user} do
# Simulate logging in the user
conn = login_user(conn, user)
# Ensure user is logged in (session contains user_id)
assert get_session(conn, :user_id) == user.id
# Simulate a GET request to the advertisements page
conn = get(conn, ~p"/advertisements")
# Check the response
response = html_response(conn, 200)
# Parse the response with Floki to extract ad titles
ad_titles = Floki.find(response, ".advertisement-box .advertisement-title")
|> Enum.map(&Floki.text(&1)) # Extract the text of each title
# Ensure all ads are in the response
assert available_ad.title in ad_titles
assert reserved_ad.title in ad_titles
assert sold_ad.title in ad_titles
# Find the indices of each advertisement title
available_ad_index = Enum.find_index(ad_titles, fn title -> title == available_ad.title end)
reserved_ad_index = Enum.find_index(ad_titles, fn title -> title == reserved_ad.title end)
sold_ad_index = Enum.find_index(ad_titles, fn title -> title == sold_ad.title end)
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment