diff --git a/.github/workflows/test-prs.yml b/.github/workflows/test-prs.yml index 9ac78176..df61a474 100644 --- a/.github/workflows/test-prs.yml +++ b/.github/workflows/test-prs.yml @@ -23,6 +23,12 @@ jobs: --health-retries 5 steps: - uses: actions/checkout@v3 + - name: Wait for PostgreSQL + run: | + until pg_isready -h localhost -p 5432; do + echo "Waiting for PostgreSQL to be ready..." + sleep 5 + done - name: Set up Python 3.10 uses: actions/setup-python@v3 with: diff --git a/apps/commerce/migrations/0002_initial.py b/apps/commerce/migrations/0002_initial.py index 03d00d7f..94349c91 100644 --- a/apps/commerce/migrations/0002_initial.py +++ b/apps/commerce/migrations/0002_initial.py @@ -33,9 +33,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="productaccountcredit", name="actioned_by", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), migrations.AddField( model_name="productaccountcredit", @@ -203,16 +201,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name="contributoraccount", name="owner", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), migrations.AddField( model_name="cart", name="creator", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), migrations.AddField( model_name="cart", diff --git a/apps/product_management/migrations/0001_initial.py b/apps/product_management/migrations/0001_initial.py index b7083a5e..18e2b984 100644 --- a/apps/product_management/migrations/0001_initial.py +++ b/apps/product_management/migrations/0001_initial.py @@ -304,9 +304,7 @@ class Migration(migrations.Migration): ("uuid", models.UUIDField(default=uuid.uuid4, editable=False)), ( "photo", - models.ImageField( - blank=True, null=True, upload_to="avatars/" - ), + models.ImageField(blank=True, null=True, upload_to="avatars/"), ), ("name", models.TextField()), ("short_description", models.TextField()), diff --git a/apps/product_management/migrations/0002_initial.py b/apps/product_management/migrations/0002_initial.py index 33f50a5a..a6bb43c0 100644 --- a/apps/product_management/migrations/0002_initial.py +++ b/apps/product_management/migrations/0002_initial.py @@ -16,9 +16,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="idea", name="person", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), migrations.AddField( model_name="idea", @@ -228,9 +226,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="bounty", name="expertise", - field=models.ManyToManyField( - related_name="bounty_expertise", to="talent.expertise" - ), + field=models.ManyToManyField(related_name="bounty_expertise", to="talent.expertise"), ), migrations.AddField( model_name="bounty", diff --git a/apps/product_management/migrations/0005_alter_product_photo.py b/apps/product_management/migrations/0005_alter_product_photo.py index c66a687a..4c5758f0 100644 --- a/apps/product_management/migrations/0005_alter_product_photo.py +++ b/apps/product_management/migrations/0005_alter_product_photo.py @@ -12,8 +12,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="product", name="photo", - field=models.ImageField( - blank=True, null=True, upload_to="products/" - ), + field=models.ImageField(blank=True, null=True, upload_to="products/"), ), ] diff --git a/apps/product_management/migrations/0013_alter_capability_description.py b/apps/product_management/migrations/0013_alter_capability_description.py index 89565d3f..b7cc593d 100644 --- a/apps/product_management/migrations/0013_alter_capability_description.py +++ b/apps/product_management/migrations/0013_alter_capability_description.py @@ -12,8 +12,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="capability", name="description", - field=models.TextField( - blank=True, default="", max_length=1000, null=True - ), + field=models.TextField(blank=True, default="", max_length=1000, null=True), ), ] diff --git a/apps/product_management/migrations/0015_producttree_productarea_product_tree.py b/apps/product_management/migrations/0015_producttree_productarea_product_tree.py index e31d4364..1ba0cbd7 100644 --- a/apps/product_management/migrations/0015_producttree_productarea_product_tree.py +++ b/apps/product_management/migrations/0015_producttree_productarea_product_tree.py @@ -17,9 +17,7 @@ class Migration(migrations.Migration): ( "id", models.UUIDField( - default=uuid.UUID( - "09332c8a-0484-43c7-b283-42e07798686e" - ), + default=uuid.UUID("09332c8a-0484-43c7-b283-42e07798686e"), primary_key=True, serialize=False, ), diff --git a/apps/product_management/migrations/0020_productareaattachment_file.py b/apps/product_management/migrations/0020_productareaattachment_file.py index c3ccaf0c..502b1a1f 100644 --- a/apps/product_management/migrations/0020_productareaattachment_file.py +++ b/apps/product_management/migrations/0020_productareaattachment_file.py @@ -15,9 +15,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="productareaattachment", name="file", - field=models.FileField( - blank=True, null=True, upload_to="attachments" - ), + field=models.FileField(blank=True, null=True, upload_to="attachments"), ), migrations.RemoveField( model_name="productareaattachment", diff --git a/apps/product_management/migrations/0024_bounty_data_migration.py b/apps/product_management/migrations/0024_bounty_data_migration.py index f4a7426e..ed93d0ad 100644 --- a/apps/product_management/migrations/0024_bounty_data_migration.py +++ b/apps/product_management/migrations/0024_bounty_data_migration.py @@ -6,9 +6,7 @@ def forward_func(apps, schema_editor): Bounty = apps.get_model("product_management.Bounty") for bounty in Bounty.objects.all(): - expertise_as_str = ", ".join( - [exp.name.title() for exp in bounty.expertise.all()] - ) + expertise_as_str = ", ".join([exp.name.title() for exp in bounty.expertise.all()]) skill_name = "" if expertise_as_str: expertise_as_str = f"({expertise_as_str})" @@ -16,9 +14,7 @@ def forward_func(apps, schema_editor): if bounty.skill: skill_name = bounty.skill.name - bounty.title = ( - f"{skill_name} {expertise_as_str} - {bounty.challenge.title}" - ) + bounty.title = f"{skill_name} {expertise_as_str} - {bounty.challenge.title}" bounty.save() diff --git a/apps/product_management/views.py b/apps/product_management/views.py index 62bede06..381f2bdc 100644 --- a/apps/product_management/views.py +++ b/apps/product_management/views.py @@ -626,6 +626,10 @@ class BountyClaimView(LoginRequiredMixin, View): def post(self, request, pk, *args, **kwargs): form = forms.BountyClaimForm(request.POST) + + form.is_valid() + print("=======================:", request.POST) + print("=======================:", form.errors) if not form.is_valid(): return JsonResponse({"errors": form.errors}, status=400) diff --git a/apps/security/migrations/0002_initial.py b/apps/security/migrations/0002_initial.py index 6ffb97d1..512c31b6 100644 --- a/apps/security/migrations/0002_initial.py +++ b/apps/security/migrations/0002_initial.py @@ -18,9 +18,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="productroleassignment", name="person", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), migrations.AddField( model_name="productroleassignment", diff --git a/apps/security/migrations/0003_alter_productroleassignment_person.py b/apps/security/migrations/0003_alter_productroleassignment_person.py index 3ef9c70d..343ffa80 100644 --- a/apps/security/migrations/0003_alter_productroleassignment_person.py +++ b/apps/security/migrations/0003_alter_productroleassignment_person.py @@ -14,8 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="productroleassignment", name="person", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), ] diff --git a/apps/talent/migrations/0001_initial.py b/apps/talent/migrations/0001_initial.py index e821e75e..fc13ee12 100644 --- a/apps/talent/migrations/0001_initial.py +++ b/apps/talent/migrations/0001_initial.py @@ -84,9 +84,7 @@ class Migration(migrations.Migration): ("preferred_name", models.CharField(max_length=128)), ( "photo", - models.ImageField( - blank=True, null=True, upload_to="avatars/" - ), + models.ImageField(blank=True, null=True, upload_to="avatars/"), ), ("headline", models.TextField()), ("overview", models.TextField(blank=True)), @@ -208,9 +206,7 @@ class Migration(migrations.Migration): ("website", models.CharField(max_length=200)), ( "type", - models.IntegerField( - choices=[(0, "Personal"), (1, "Company")] - ), + models.IntegerField(choices=[(0, "Personal"), (1, "Company")]), ), ( "person", diff --git a/apps/talent/migrations/0002_bountydeliveryattempt_attachment_and_more.py b/apps/talent/migrations/0002_bountydeliveryattempt_attachment_and_more.py index 243c07a6..87bcf9c9 100644 --- a/apps/talent/migrations/0002_bountydeliveryattempt_attachment_and_more.py +++ b/apps/talent/migrations/0002_bountydeliveryattempt_attachment_and_more.py @@ -32,9 +32,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="bountydeliveryattempt", name="person", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="talent.person" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="talent.person"), ), migrations.DeleteModel( name="BountyDeliveryAttachment", diff --git a/e2e/base.py b/e2e/base.py deleted file mode 100644 index 8fc45feb..00000000 --- a/e2e/base.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -from django.contrib.staticfiles.testing import StaticLiveServerTestCase - -from playwright.sync_api import sync_playwright - - -class BaseE2ETest(StaticLiveServerTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - cls.playwright = sync_playwright().start() - - @classmethod - def tearDownClass(cls): - cls.playwright.stop() - super().tearDownClass() - - def setUp(self): - super().setUp() - self.browser = self.playwright.chromium.launch() - - def tearDown(self): - self.browser.close() - super().tearDown() diff --git a/e2e/conftest.py b/e2e/conftest.py new file mode 100644 index 00000000..d07d4822 --- /dev/null +++ b/e2e/conftest.py @@ -0,0 +1 @@ +from e2e.fixtures import * # noqa diff --git a/e2e/fixtures.py b/e2e/fixtures.py new file mode 100644 index 00000000..5d39546e --- /dev/null +++ b/e2e/fixtures.py @@ -0,0 +1,87 @@ +import os + +from django.utils import timezone + +import pytest +from model_bakery import baker +from playwright.sync_api import sync_playwright + + +@pytest.fixture(scope="module") +def playwright_context(): + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + playwright = sync_playwright().start() + yield playwright + playwright.stop() + + +@pytest.fixture +def browser_context(playwright_context): + # browser = playwright_context.chromium.launch(headless=False,slow_mo=300) + browser = playwright_context.chromium.launch() + yield browser + browser.close() + + +@pytest.fixture +def page_context(browser_context): + page = browser_context.new_page() + yield page + + +@pytest.fixture +def create_user(db): + username = "testuser" + password = "12345" + first_name = "Test" + last_name = "User" + full_name = f"{first_name} {last_name}" + + user = baker.make( + "security.User", + username=username, + password=password, + first_name=first_name, + last_name=last_name, + ) + user.set_password(password) + user.save() + person = baker.make("talent.Person", user=user, photo="image.png", full_name=full_name) + + return user, username, password, person + + +@pytest.fixture +def setup_bounty(db, create_user): + user, username, password, person = create_user + + now = timezone.now() + product = baker.make( + "product_management.Product", + name="PetConnect", + slug="petconnect", + created_at=now, + updated_at=now, + ) + product_tree = baker.make("product_management.ProductTree", product=product, name="PetConnect Tree") + product_area = baker.make("product_management.ProductArea", product_tree=product_tree, name="PetConnect Area") + initiative = baker.make("product_management.Initiative", product=product, name="PetConnect Service Integration") + challenge = baker.make( + "product_management.Challenge", + product_area=product_area, + initiative=initiative, + product=product, + status="Active", + title="Pet Emergency Alert System", + short_description="Introduce pet grooming scheduler feature.", + priority="High", + created_by=person, + ) + bounty = baker.make( + "product_management.Bounty", + challenge=challenge, + title="Pet Sitting Made Easy", + status="Available", + description="Information Systems & Networking (Python)", + ) + return product, challenge, bounty, person, username, password diff --git a/e2e/helpers.py b/e2e/helpers.py new file mode 100644 index 00000000..a9f2fd5c --- /dev/null +++ b/e2e/helpers.py @@ -0,0 +1,7 @@ +from e2e.pages.login_page import LoginPage + + +def login_user(page, live_server_url, username, password): + login_page = LoginPage(page) + login_page.navigate(f"{live_server_url}{login_page.url}") + login_page.login(username, password) diff --git a/e2e/pages/challenge_details_page.py b/e2e/pages/challenge_details_page.py new file mode 100644 index 00000000..f7abe237 --- /dev/null +++ b/e2e/pages/challenge_details_page.py @@ -0,0 +1,24 @@ +from e2e.pages.base import BasePage + + +class ChallengeDetailPage(BasePage): + def __init__(self, page): + super().__init__(page) + self.bounty_add_btn = self.page.locator("#bounty-add-btn") + self.expected_submission_date = self.page.locator("#expected_submission_date") + self.terms_check_box = self.page.locator("#is_agreement_accepted") + self.request_claim_button = self.page.locator('button[class="ajs-button ajs-ok"]') + + def get_challenge_detail_button(self, product_slug, challenge_id): + href_value = f"/{product_slug}/challenge/{challenge_id}" + return self.page.locator(f'a[href="{href_value}"].relative') + + def get_bounty_claim_button(self, bounty_id): + href_value = f"/bounty-claim/{bounty_id}/" + return self.page.locator(f'button[hx-post="{href_value}"]') + + def get_actions_dropdown(self, bounty_id): + return self.page.locator(f"#dropdownHoverButton_{bounty_id}") + + def get_cancel_request_button(self, bounty_claim_id): + return self.page.locator(f'a[hx-post="/bounty_claim/delete/{bounty_claim_id}"]') diff --git a/e2e/pages/login_page.py b/e2e/pages/login_page.py index cbbf0726..1c7996c7 100644 --- a/e2e/pages/login_page.py +++ b/e2e/pages/login_page.py @@ -1,10 +1,9 @@ -from .base import BasePage +from e2e.pages.base import BasePage class LoginPage(BasePage): def __init__(self, page): super().__init__(page) - # self.url = "/sign-in/?next=/dashboard/" self.url = "/sign-in/" self.username_field = self.page.locator("#id_username_or_email") self.password_field = self.page.locator("#id_password") diff --git a/e2e/test_login.py b/e2e/test_login.py deleted file mode 100644 index 12ad67bd..00000000 --- a/e2e/test_login.py +++ /dev/null @@ -1,20 +0,0 @@ -from model_bakery import baker - -from e2e.base import BaseE2ETest -from e2e.pages.login_page import LoginPage - - -class TestLogin(BaseE2ETest): - def setUp(self): - super().setUp() - self.page = self.browser.new_page() - self.user_one = baker.make("security.User", username="testuser1", password="12345") - self.user_one.set_password("12345") - self.user_one.save() - self.person = baker.make("talent.Person", user=self.user_one) - - def test_login(self): - login_page = LoginPage(self.page) - login_page.navigate(f"{self.live_server_url}{login_page.url}") - login_page.login("testuser1", "12345") - # self.assertTrue(self.page.is_visible("#navbar-menu-button")) diff --git a/e2e/test_signup.py b/e2e/test_signup.py deleted file mode 100644 index 292975dc..00000000 --- a/e2e/test_signup.py +++ /dev/null @@ -1,14 +0,0 @@ -from e2e.base import BaseE2ETest -from e2e.pages.signup_page import SignupPage - - -class TestLogin(BaseE2ETest): - def setUp(self): - super().setUp() - self.page = self.browser.new_page() - - def test_signup(self): - signup_page = SignupPage(self.page) - signup_page.navigate(f"{self.live_server_url}{signup_page.url}") - signup_page.signup("Pacey Witter", "pacey@gmail.com", "pacey", "pacey", "4@Password") - # self.assertTrue(self.page.is_visible("#navbar-menu-button")) diff --git a/e2e/tests/test_claim_bounty.py b/e2e/tests/test_claim_bounty.py new file mode 100644 index 00000000..7fd14fa8 --- /dev/null +++ b/e2e/tests/test_claim_bounty.py @@ -0,0 +1,52 @@ +from datetime import datetime, timedelta + +from apps.talent.models import BountyClaim +from e2e.helpers import login_user +from e2e.pages.challenge_details_page import ChallengeDetailPage + + +def test_claim_bounty(live_server, page_context, setup_bounty): + product, challenge, bounty, _, username, password = setup_bounty + login_user(page_context, live_server.url, username, password) + + page = ChallengeDetailPage(page_context) + page_context.wait_for_timeout(1500) + assert BountyClaim.objects.filter(bounty=bounty.id).count() == 0 + + page_context.reload() + page_context.wait_for_timeout(5000) + challenge_detail_button = page.get_challenge_detail_button(product.slug, challenge.id) + + challenge_detail_button.click() + page_context.wait_for_timeout(1500) + + print("=======================:", bounty) + bounty_claim_button = page.get_bounty_claim_button(bounty.id) + bounty_claim_button.click() + print("=======================:", page.get_bounty_claim_button(bounty.id)) + + page_context.wait_for_timeout(500) + page.bounty_add_btn.click() + page_context.wait_for_timeout(500) + + future_date = datetime.now() + timedelta(days=10) + page_context.wait_for_timeout(500) + + day = future_date.strftime("%d") + month = future_date.strftime("%m") + year = future_date.strftime("%Y") + + print("========================:", datetime.now()) + print("========================:", day, month, year) + date = f"{year}/{month}/{day}" + # Clear the date input fields before typing + page.expected_submission_date.fill(f"{year}-{month}-{day}") + + page.terms_check_box.check() + + page.request_claim_button.click() + + page_context.wait_for_timeout(1500) + + bounty_claim = BountyClaim.objects.get(bounty=bounty.id) + assert bounty_claim.status == BountyClaim.Status.REQUESTED diff --git a/e2e/tests/test_login.py b/e2e/tests/test_login.py new file mode 100644 index 00000000..d06ef0d2 --- /dev/null +++ b/e2e/tests/test_login.py @@ -0,0 +1,8 @@ +from e2e.helpers import login_user +from e2e.pages.login_page import LoginPage + + +def test_login(live_server, page_context, create_user): + _, username, password, _ = create_user + login_user(page_context, live_server.url, username, password) + # assert page_context.is_visible("#navbar-menu-button") diff --git a/e2e/tests/test_signup.py b/e2e/tests/test_signup.py new file mode 100644 index 00000000..70e48f6d --- /dev/null +++ b/e2e/tests/test_signup.py @@ -0,0 +1,8 @@ +from e2e.pages.signup_page import SignupPage + + +def test_signup(live_server, page_context): + signup_page = SignupPage(page_context) + signup_page.navigate(f"{live_server.url}{signup_page.url}") + signup_page.signup("Pacey Witter", "pacey@gmail.com", "pacey", "pacey", "4@Password") + # assert page_context.is_visible("#navbar-menu-button")