Skip to content

Commit

Permalink
Merge branch 'kalvis/mantine-setup' into leszek/task-1377-simple-tabl…
Browse files Browse the repository at this point in the history
…e-new-(mantine-poc)
  • Loading branch information
magicznyleszek authored Dec 19, 2024
2 parents b07fd65 + b5695da commit 2c89090
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 65 deletions.
37 changes: 29 additions & 8 deletions .github/workflows/darker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
# In addition to checking out the 'merge commit', we also want to
# fetch enough commits to find the most recent commit in the base
# branch (typically 'main')
fetch-depth: 100

- name: Set up Python
uses: actions/setup-python@v5
Expand All @@ -18,12 +21,30 @@ jobs:
- name: Install pip dependencies
run: python -m pip install darker[isort] flake8 flake8-quotes isort --quiet

# use `--ignore=F821` to avoid raising false positive error in typing
# annotations with string, e.g. def my_method(my_model: 'ModelName')

# darker still exit with code 1 even with no errors on changes
- name: Run Darker with base commit
- name: Run Darker, comparing '${{github.ref_name}}' with the latest in '${{github.event.pull_request.base.ref}}'
run: |
output=$(darker --check --isort -L "flake8 --max-line-length=88 --extend-ignore=F821" kpi kobo hub -r ${{ github.event.pull_request.base.sha }})
# Run darker only on file changes introduced in this PR.
# Allows incremental adoption of linter rules on a per-file basis.
# Prevents this scenario:
# - PR A is opened, modifying file A; CI passes.
# - PR B is opened & merged, with linter errors in file B.
# - PR A is updated again, modifying file A. CI fails from file B.
# Get the latest commit in the base branch (usually 'main') at time of
# CI run, to compare with this PR's merge branch.
# GitHub doesn't provide a nice name for this SHA, but we can find it:
# https://www.kenmuse.com/blog/the-many-shas-of-a-github-pull-request/#extracting-the-base-sha
MERGE_PARENTS=($(git rev-list --parents -1 ${{ github.sha }}))
LATEST_IN_BASE_BRANCH=${MERGE_PARENTS[1]}
# Run darker. (https://github.com/akaihola/darker)
# -L runs the linter
# `--ignore=F821` avoids raising false positive error in typing
# annotations with string, e.g. def my_method(my_model: 'ModelName')
# -r REV specifies a commit to compare with the worktree
output=$(darker --check --isort -L "flake8 --max-line-length=88 --extend-ignore=F821" kpi kobo hub -r $LATEST_IN_BASE_BRANCH)
# darker still exits with code 1 even with no errors on changes
# So, make this fail CI only if there is output from darker.
[[ -n "$output" ]] && echo "$output" && exit 1 || exit 0
shell: /usr/bin/bash {0}
3 changes: 0 additions & 3 deletions jsapp/js/components/formViewSideTabs.es6
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ class FormViewSideTabs extends Reflux.Component {
renderFormSideTabs() {
var sideTabs = [];

const isActivityLogsEnabled = checkFeatureFlag(FeatureFlag.activityLogsEnabled);

if (
this.state.asset &&
this.state.asset.has_deployment &&
Expand Down Expand Up @@ -167,7 +165,6 @@ class FormViewSideTabs extends Reflux.Component {
}

if (
isActivityLogsEnabled &&
userCan(
PERMISSIONS_CODENAMES.manage_asset,
this.state.asset
Expand Down
1 change: 0 additions & 1 deletion jsapp/js/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* For our sanity, use camel case and match key with value.
*/
export enum FeatureFlag {
activityLogsEnabled = 'activityLogsEnabled',
mmosEnabled = 'mmosEnabled',
oneTimeAddonsEnabled = 'oneTimeAddonsEnabled',
exportActivityLogsEnabled = 'exportActivityLogsEnabled',
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/projects/myOrgProjectsRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function MyOrgProjectsRoute() {
viewUid={ORG_VIEW.uid}
baseUrl={`${ROOT_URL}${apiUrl}`}
defaultVisibleFields={HOME_DEFAULT_VISIBLE_FIELDS}
includeTypeFilter={false}
includeTypeFilter
defaultOrderableFields={HOME_ORDERABLE_FIELDS}
defaultExcludedFields={HOME_EXCLUDED_FIELDS}
isExportButtonVisible={false}
Expand Down
24 changes: 16 additions & 8 deletions jsapp/js/projects/projectViews/viewSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import KoboDropdown from 'js/components/common/koboDropdown';

// Stores and hooks
import projectViewsStore from './projectViewsStore';
import {useOrganizationQuery} from 'js/account/organization/organizationQuery';
import {
useOrganizationQuery,
OrganizationUserRole,
} from 'js/account/organization/organizationQuery';

// Constants
import {PROJECTS_ROUTES} from 'js/router/routerConstants';
Expand Down Expand Up @@ -49,10 +52,15 @@ function ViewSwitcher(props: ViewSwitcherProps) {
}
};

const hasMultipleOptions = (
projectViews.views.length !== 0 ||
orgQuery.data?.is_mmo
);
const displayMyOrgOption =
orgQuery.data?.is_mmo &&
[OrganizationUserRole.admin, OrganizationUserRole.owner].includes(
orgQuery.data?.request_user_role
);

const hasMultipleOptions =
projectViews.views.length !== 0 || displayMyOrgOption;

const organizationName = orgQuery.data?.name || t('Organization');

let triggerLabel = HOME_VIEW.name;
Expand Down Expand Up @@ -109,9 +117,9 @@ function ViewSwitcher(props: ViewSwitcherProps) {
{HOME_VIEW.name}
</button>

{/* This is the organization view option - depends if user is in MMO
organization */}
{orgQuery.data?.is_mmo &&
{/* This is the organization view option - restricted to
MMO admins and owners */}
{displayMyOrgOption &&
<button
key={ORG_VIEW.uid}
className={styles.menuOption}
Expand Down
5 changes: 5 additions & 0 deletions jsapp/js/projects/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Navigate, Route} from 'react-router-dom';
import RequireAuth from 'js/router/requireAuth';
import {PROJECTS_ROUTES} from 'js/router/routerConstants';
import {RequireOrgPermissions} from 'js/router/RequireOrgPermissions.component';
import { OrganizationUserRole } from '../account/organization/organizationQuery';

const MyProjectsRoute = React.lazy(
() => import(/* webpackPrefetch: true */ './myProjectsRoute')
Expand Down Expand Up @@ -34,6 +35,10 @@ export default function routes() {
element={
<RequireAuth>
<RequireOrgPermissions
validRoles={[
OrganizationUserRole.owner,
OrganizationUserRole.admin,
]}
mmoOnly
redirectRoute={PROJECTS_ROUTES.MY_PROJECTS}
>
Expand Down
7 changes: 4 additions & 3 deletions kobo/apps/stripe/tests/test_organization_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from kobo.apps.organizations.models import Organization, OrganizationUser
from kobo.apps.stripe.constants import USAGE_LIMIT_MAP
from kobo.apps.stripe.tests.utils import (
generate_free_plan,
generate_mmo_subscription,
generate_plan_subscription,
)
Expand Down Expand Up @@ -461,11 +462,11 @@ def setUp(self):
self.organization.add_user(self.anotheruser, is_admin=True)

def test_get_plan_community_limit(self):
generate_mmo_subscription(self.organization)
generate_free_plan()
limit = get_organization_plan_limit(self.organization, 'seconds')
assert limit == 2000 # TODO get the limits from the community plan, overrides
assert limit == 600
limit = get_organization_plan_limit(self.organization, 'characters')
assert limit == 2000 # TODO get the limits from the community plan, overrides
assert limit == 6000

@data('characters', 'seconds')
def test_get_suscription_limit(self, usage_type):
Expand Down
20 changes: 20 additions & 0 deletions kobo/apps/stripe/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@
from kobo.apps.organizations.models import Organization


def generate_free_plan():
product_metadata = {
'product_type': 'plan',
'submission_limit': '5000',
'asr_seconds_limit': '600',
'mt_characters_limit': '6000',
'storage_bytes_limit': '1000000000',
}

product = baker.make(Product, active=True, metadata=product_metadata)

baker.make(
Price,
active=True,
recurring={'interval': 'month'},
unit_amount=0,
product=product,
)


def generate_plan_subscription(
organization: Organization,
metadata: dict = None,
Expand Down
71 changes: 32 additions & 39 deletions kobo/apps/stripe/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from kobo.apps.organizations.models import Organization
from kobo.apps.organizations.types import UsageType
from kobo.apps.stripe.constants import ACTIVE_STRIPE_STATUSES, USAGE_LIMIT_MAP
from kobo.apps.stripe.constants import USAGE_LIMIT_MAP


def generate_return_url(product_metadata):
Expand All @@ -30,51 +30,44 @@ def get_organization_plan_limit(
organization: Organization, usage_type: UsageType
) -> int | float:
"""
Get organization plan limit for a given usage type
Get organization plan limit for a given usage type,
will fall back to infinite value if no subscription or
default free tier plan found.
"""
if not settings.STRIPE_ENABLED:
return None
stripe_key = f'{USAGE_LIMIT_MAP[usage_type]}_limit'
query_product_type = (
'djstripe_customers__subscriptions__items__price__'
'product__metadata__product_type'
)
query_status__in = 'djstripe_customers__subscriptions__status__in'
organization_filter = Organization.objects.filter(
id=organization.id,
**{
query_status__in: ACTIVE_STRIPE_STATUSES,
query_product_type: 'plan',
},
)

field_price_limit = (
'djstripe_customers__subscriptions__items__' f'price__metadata__{stripe_key}'
)
field_product_limit = (
'djstripe_customers__subscriptions__items__'
f'price__product__metadata__{stripe_key}'
)
current_limit = organization_filter.values(
price_limit=F(field_price_limit),
product_limit=F(field_product_limit),
prod_metadata=F(
'djstripe_customers__subscriptions__items__price__product__metadata'
),
).first()
return inf

limit_key = f'{USAGE_LIMIT_MAP[usage_type]}_limit'

relevant_limit = None
if current_limit is not None:
relevant_limit = current_limit.get('price_limit') or current_limit.get(
'product_limit'
if subscription := organization.active_subscription_billing_details():
price_metadata = subscription['price_metadata']
product_metadata = subscription['product_metadata']
price_limit = price_metadata[limit_key] if price_metadata else None
product_limit = product_metadata[limit_key] if product_metadata else None
relevant_limit = price_limit or product_limit
else:
from djstripe.models.core import Product

# Anyone who does not have a subscription is on the free tier plan by default
default_plan = (
Product.objects.filter(
prices__unit_amount=0, prices__recurring__interval='month'
)
.values(limit=F(f'metadata__{limit_key}'))
.first()
)
if relevant_limit is None:
# TODO: get the limits from the community plan, overrides
relevant_limit = 2000
# Limits in Stripe metadata are strings. They may be numbers or 'unlimited'

if default_plan:
relevant_limit = default_plan['limit']

if relevant_limit == 'unlimited':
return inf

return int(relevant_limit)
if relevant_limit:
return int(relevant_limit)

return inf


def get_total_price_for_quantity(price: 'djstripe.models.Price', quantity: int):
Expand Down
7 changes: 5 additions & 2 deletions kpi/views/v2/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.http import Http404, HttpResponseRedirect
from django.utils.translation import gettext_lazy as t
from pymongo.errors import OperationFailure
from rest_framework import renderers, serializers, status, viewsets
from rest_framework import renderers, serializers, status
from rest_framework.decorators import action
from rest_framework.pagination import _positive_int as positive_int
from rest_framework.request import Request
Expand All @@ -17,6 +17,7 @@
from rest_framework_extensions.mixins import NestedViewSetMixin

from kobo.apps.audit_log.audit_actions import AuditAction
from kobo.apps.audit_log.base_views import AuditLoggedViewSet
from kobo.apps.audit_log.models import AuditLog, AuditType
from kobo.apps.openrosa.libs.utils.logger_tools import http_open_rosa_error_handler
from kpi.authentication import EnketoSessionAuthentication
Expand Down Expand Up @@ -54,7 +55,7 @@


class DataViewSet(
AssetNestedObjectViewsetMixin, NestedViewSetMixin, viewsets.GenericViewSet
AssetNestedObjectViewsetMixin, NestedViewSetMixin, AuditLoggedViewSet
):
"""
## List of submissions for a specific asset
Expand Down Expand Up @@ -330,6 +331,8 @@ class DataViewSet(
)
permission_classes = (SubmissionPermission,)
pagination_class = DataPagination
log_type = AuditType.PROJECT_HISTORY
logged_fields = []

@action(detail=False, methods=['PATCH', 'DELETE'],
renderer_classes=[renderers.JSONRenderer])
Expand Down

0 comments on commit 2c89090

Please sign in to comment.