Skip to content

Commit

Permalink
add root_organization property to worker, clean up decoration service…
Browse files Browse the repository at this point in the history
…, change notification urls for GL (#990)
  • Loading branch information
nora-codecov authored Jan 16, 2025
1 parent 2c7bd18 commit 40613b5
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 82 deletions.
39 changes: 37 additions & 2 deletions database/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import uuid
from datetime import datetime
from functools import cached_property
from typing import Optional

from shared.plan.constants import PlanName
from sqlalchemy import Column, ForeignKey, Index, UniqueConstraint, types
Expand Down Expand Up @@ -105,6 +106,7 @@ class Owner(CodecovBaseModel):
avatar_url = Column(types.Text, server_default=FetchedValue())
updatestamp = Column(types.DateTime, server_default=FetchedValue())
parent_service_id = Column(types.Text, server_default=FetchedValue())
root_parent_service_id = Column(types.Text, nullable=True)
plan_provider = Column(types.Text, server_default=FetchedValue())
trial_start_date = Column(types.DateTime, server_default=FetchedValue())
trial_end_date = Column(types.DateTime, server_default=FetchedValue())
Expand Down Expand Up @@ -153,10 +155,43 @@ class Owner(CodecovBaseModel):
)

@property
def slug(self):
def slug(self: "Owner") -> str:
return self.username

def __repr__(self):
@property
def root_organization(self: "Owner") -> Optional["Owner"]:
"""
Find the root organization of Gitlab OwnerOrg, by using the root_parent_service_id
if it exists, otherwise iterating through the parents and cache it in root_parent_service_id
"""
db_session = self.get_db_session()
if self.root_parent_service_id:
return self._get_owner_by_service_id(
db_session, self.root_parent_service_id
)

root = None
if self.service == "gitlab" and self.parent_service_id:
root = self
while root.parent_service_id is not None:
root = self._get_owner_by_service_id(db_session, root.parent_service_id)
self.root_parent_service_id = root.service_id
db_session.commit()
return root

def _get_owner_by_service_id(
self: "Owner", db_session: Session, service_id: str
) -> "Owner":
"""
Helper method to fetch an Owner by service_id.
"""
return (
db_session.query(Owner)
.filter_by(service_id=service_id, service=self.service)
.one()
)

def __repr__(self: "Owner") -> str:
return f"Owner<{self.ownerid}@service<{self.service}>>"


Expand Down
2 changes: 2 additions & 0 deletions database/tests/factories/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import factory
from factory import Factory
from shared.plan.constants import PlanName

from database import enums, models
from services.encryption import encryptor
Expand Down Expand Up @@ -49,6 +50,7 @@ class Meta:
trial_status = enums.TrialStatus.NOT_STARTED.value
trial_fired_by = None
upload_token_required_for_public_repos = False
plan = PlanName.BASIC_PLAN_NAME.value

oauth_token = factory.LazyAttribute(
lambda o: encrypt_oauth_token(o.unencrypted_oauth_token)
Expand Down
42 changes: 42 additions & 0 deletions database/tests/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,48 @@ def test_upload_token_required_for_public_repos(self, dbsession):
assert tokens_not_required_owner.onboarding_completed is True
assert tokens_not_required_owner.upload_token_required_for_public_repos is False

def test_root_organization(self, dbsession):
gitlab_root_group = OwnerFactory.create(
username="root_group",
service="gitlab",
plan="users-pr-inappm",
)
dbsession.add(gitlab_root_group)
gitlab_middle_group = OwnerFactory.create(
username="mid_group",
service="gitlab",
parent_service_id=gitlab_root_group.service_id,
root_parent_service_id=None,
)
dbsession.add(gitlab_middle_group)
gitlab_subgroup = OwnerFactory.create(
username="subgroup",
service="gitlab",
parent_service_id=gitlab_middle_group.service_id,
root_parent_service_id=None,
)
dbsession.add(gitlab_subgroup)
github_org = OwnerFactory.create(
username="gh",
service="github",
)
dbsession.add(github_org)
dbsession.flush()

assert gitlab_root_group.root_organization is None
assert gitlab_root_group.root_parent_service_id is None

assert gitlab_middle_group.root_organization == gitlab_root_group
assert (
gitlab_middle_group.root_parent_service_id == gitlab_root_group.service_id
)

assert gitlab_subgroup.root_organization == gitlab_root_group
assert gitlab_subgroup.root_parent_service_id == gitlab_root_group.service_id

assert github_org.root_organization is None
assert github_org.root_parent_service_id is None


class TestAccountModels(object):
def test_create_account(self, dbsession):
Expand Down
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
https://github.com/codecov/test-results-parser/archive/190bbc8a911099749928e13d5fe57f6027ca1e74.tar.gz#egg=test-results-parser
https://github.com/codecov/shared/archive/de4b37bc5a736317c6e7c93f9c58e9ae07f8c96b.tar.gz#egg=shared
https://github.com/codecov/shared/archive/191837f5e35f5efc410e670aac7e50e0d09e43e1.tar.gz#egg=shared
https://github.com/codecov/timestring/archive/d37ceacc5954dff3b5bd2f887936a98a668dda42.tar.gz#egg=timestring
asgiref>=3.7.2
analytics-python==1.3.0b1
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ sentry-sdk==2.13.0
# shared
setuptools==75.7.0
# via nodeenv
shared @ https://github.com/codecov/shared/archive/de4b37bc5a736317c6e7c93f9c58e9ae07f8c96b.tar.gz#egg=shared
shared @ https://github.com/codecov/shared/archive/191837f5e35f5efc410e670aac7e50e0d09e43e1.tar.gz#egg=shared
# via -r requirements.in
six==1.16.0
# via
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from shared.yaml import UserYaml

from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME
from database.tests.factories.core import CommitFactory
from database.tests.factories.core import CommitFactory, PullFactory
from services.bundle_analysis.comparison import ComparisonLoader
from services.bundle_analysis.notify.conftest import (
get_commit_pair,
Expand All @@ -20,6 +20,7 @@
from services.bundle_analysis.notify.contexts.comment import (
BundleAnalysisPRCommentContextBuilder,
)
from services.repository import EnrichedPull
from services.seats import SeatActivationInfo, ShouldActivateSeat


Expand Down Expand Up @@ -305,15 +306,27 @@ def test_build_context(self, dbsession, mocker, mock_storage):
)

def test_initialize_from_context(self, dbsession, mocker):
head_commit, _ = get_commit_pair(dbsession)
head_commit, base_commit = get_commit_pair(dbsession)
user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG)
builder = BundleAnalysisPRCommentContextBuilder().initialize(
head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME
)
context = builder.get_result()
context.commit_report = MagicMock(name="fake_commit_report")
context.bundle_analysis_report = MagicMock(name="fake_bundle_analysis_report")
context.pull = MagicMock(name="fake_pull")

pull = PullFactory(
repository=base_commit.repository,
head=head_commit.commitid,
base=base_commit.commitid,
compared_to=base_commit.commitid,
)
dbsession.add(pull)
dbsession.commit()
context.pull = EnrichedPull(
database_pull=pull,
provider_pull={},
)

other_builder = BundleAnalysisPRCommentContextBuilder().initialize_from_context(
user_yaml, context
Expand All @@ -327,7 +340,9 @@ def test_initialize_from_context(self, dbsession, mocker):
with pytest.raises(ContextNotLoadedError):
other_context.bundle_analysis_comparison

fake_comparison = MagicMock(name="fake_comparison", percentage_delta=10.0)
fake_comparison = MagicMock(
name="fake_comparison", percentage_delta=10.0, total_size_delta=10.0
)
mocker.patch.object(
ComparisonLoader, "get_comparison", return_value=fake_comparison
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from shared.yaml import UserYaml

from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME
from database.tests.factories.core import CommitFactory
from database.tests.factories.core import CommitFactory, PullFactory
from services.bundle_analysis.comparison import ComparisonLoader
from services.bundle_analysis.notify.conftest import (
get_commit_pair,
Expand All @@ -23,6 +23,7 @@
CommitStatusNotificationContextBuilder,
)
from services.bundle_analysis.notify.types import NotificationUserConfig
from services.repository import EnrichedPull
from services.seats import SeatActivationInfo, ShouldActivateSeat


Expand Down Expand Up @@ -259,15 +260,26 @@ def test_build_context(self, dbsession, mocker, mock_storage):
assert context.commit_status_url is not None

def test_initialize_from_context(self, dbsession, mocker):
head_commit, _ = get_commit_pair(dbsession)
head_commit, base_commit = get_commit_pair(dbsession)
user_yaml = UserYaml.from_dict(PATCH_CENTRIC_DEFAULT_CONFIG)
builder = CommitStatusNotificationContextBuilder().initialize(
head_commit, user_yaml, GITHUB_APP_INSTALLATION_DEFAULT_NAME
)
context = builder.get_result()
context.commit_report = MagicMock(name="fake_commit_report")
context.bundle_analysis_report = MagicMock(name="fake_bundle_analysis_report")
context.pull = MagicMock(name="fake_pull")
pull = PullFactory(
repository=base_commit.repository,
head=head_commit.commitid,
base=base_commit.commitid,
compared_to=base_commit.commitid,
)
dbsession.add(pull)
dbsession.commit()
context.pull = EnrichedPull(
database_pull=pull,
provider_pull={},
)

other_builder = (
CommitStatusNotificationContextBuilder().initialize_from_context(
Expand Down
34 changes: 12 additions & 22 deletions services/decoration.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import logging
from dataclasses import dataclass

from shared.billing import is_pr_billing_plan
from shared.config import get_config
from shared.plan.service import PlanService
from shared.upload.utils import query_monthly_coverage_measurements
from sqlalchemy import func

from database.enums import Decoration
from database.models import Owner
Expand All @@ -31,8 +29,8 @@ class DecorationDetails(object):
decoration_type: Decoration
reason: str
should_attempt_author_auto_activation: bool = False
activation_org_ownerid: int = None
activation_author_ownerid: int = None
activation_org_ownerid: int | None = None
activation_author_ownerid: int | None = None


def _is_bot_account(author: Owner) -> bool:
Expand All @@ -50,7 +48,7 @@ def determine_uploads_used(plan_service: PlanService) -> int:

def determine_decoration_details(
enriched_pull: EnrichedPull, empty_upload=None
) -> dict:
) -> DecorationDetails:
"""
Determine the decoration details from pull information. We also check if the pull author needs to be activated
Expand Down Expand Up @@ -85,7 +83,7 @@ def determine_decoration_details(
)

if db_pull.repository.private is False:
# public repo or repo we arent certain is private should be standard
# public repo or repo we aren't certain is private should be standard
return DecorationDetails(
decoration_type=Decoration.standard, reason="Public repo"
)
Expand All @@ -94,19 +92,12 @@ def determine_decoration_details(

db_session = db_pull.get_db_session()

if org.service == "gitlab" and org.parent_service_id:
# need to get root group so we can check plan info
(gl_root_group,) = db_session.query(
func.public.get_gitlab_root_group(org.ownerid)
).first()
# do not access plan directly - only through PlanService
org_plan = PlanService(current_org=org)
# use the org that has the plan - for GL this is the root_org rather than the repository.owner org
org = org_plan.current_org

org = (
db_session.query(Owner)
.filter(Owner.ownerid == gl_root_group.get("ownerid"))
.first()
)

if not is_pr_billing_plan(org.plan):
if not org_plan.is_pr_billing_plan:
return DecorationDetails(
decoration_type=Decoration.standard, reason="Org not on PR plan"
)
Expand Down Expand Up @@ -134,13 +125,12 @@ def determine_decoration_details(
reason="PR author not found in database",
)

plan_service = PlanService(current_org=org)
monthly_limit = plan_service.monthly_uploads_limit
monthly_limit = org_plan.monthly_uploads_limit
if monthly_limit is not None:
uploads_used = determine_uploads_used(plan_service=plan_service)
uploads_used = determine_uploads_used(plan_service=org_plan)

if (
uploads_used >= plan_service.monthly_uploads_limit
uploads_used >= org_plan.monthly_uploads_limit
and not requires_license()
):
return DecorationDetails(
Expand Down
21 changes: 7 additions & 14 deletions services/seats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from dataclasses import dataclass
from enum import Enum

from shared.billing import is_pr_billing_plan
from sqlalchemy import func
from shared.plan.service import PlanService
from sqlalchemy.orm import Session

from database.models import Owner
Expand Down Expand Up @@ -58,19 +57,13 @@ def determine_seat_activation(pull: EnrichedPull) -> SeatActivationInfo:
org = db_pull.repository.owner

db_session: Session = db_pull.get_db_session()
if org.service == "gitlab" and org.parent_service_id:
# need to get root group so we can check plan info
(gl_root_group,) = db_session.query(
func.public.get_gitlab_root_group(org.ownerid)
).first()

org = (
db_session.query(Owner)
.filter(Owner.ownerid == gl_root_group.get("ownerid"))
.first()
)

if not is_pr_billing_plan(org.plan):
# do not access plan directly - only through PlanService
org_plan = PlanService(current_org=org)
# use the org that has the plan - for GL this is the root_org rather than the repository.owner org
org = org_plan.current_org

if not org_plan.is_pr_billing_plan:
return SeatActivationInfo(reason="no_pr_billing_plan")

pr_author = (
Expand Down
Loading

0 comments on commit 40613b5

Please sign in to comment.