Skip to content

Commit

Permalink
temp: WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Anas12091101 committed Feb 17, 2025
1 parent 8fd2bd3 commit acb0aaf
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 54 deletions.
4 changes: 4 additions & 0 deletions sheets/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ class FailedBatchRequestException(Exception): # noqa: N818
"""
General exception for a failure during a Google batch API request
"""


class CouponAssignmentError(Exception):
"""Custom exception for coupon assignment errors."""
75 changes: 22 additions & 53 deletions sheets/management/commands/process_coupon_assignment_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@

from django.core.management import BaseCommand, CommandError

from ecommerce.models import BulkCouponAssignment
from sheets.api import ExpandedSheetsClient, get_authorized_pygsheets_client
from sheets.coupon_assign_api import CouponAssignmentHandler
from sheets.management.utils import get_assignment_spreadsheet_by_title
from sheets.utils import google_date_string_to_datetime, spreadsheet_repr
from sheets.utils import fetch_and_process_coupon_assignment


class Command(BaseCommand):
Expand Down Expand Up @@ -45,57 +41,30 @@ def add_arguments(self, parser):
super().add_arguments(parser)

def handle(self, *args, **options): # noqa: ARG002
if not options["id"] and not options["title"]:
sheet_id = options.get("id")
title = options.get("title")

if not sheet_id and not title:
raise CommandError("Need to provide --id or --title") # noqa: EM101

pygsheets_client = get_authorized_pygsheets_client()
# Fetch the correct spreadsheet
if options["id"]:
spreadsheet = pygsheets_client.open_by_key(options["id"])
else:
spreadsheet = get_assignment_spreadsheet_by_title(
pygsheets_client, options["title"]
)
# Process the sheet
self.stdout.write(
"Found spreadsheet ({}). Processing...".format( # noqa: UP032
spreadsheet_repr(spreadsheet)
)
)
use_sheet_id = bool(sheet_id)
value = sheet_id if use_sheet_id else title

expanded_sheets_client = ExpandedSheetsClient(pygsheets_client)
metadata = expanded_sheets_client.get_drive_file_metadata(
file_id=spreadsheet.id, fields="modifiedTime"
)
sheet_last_modified = google_date_string_to_datetime(metadata["modifiedTime"])
bulk_assignment, created = BulkCouponAssignment.objects.get_or_create(
assignment_sheet_id=spreadsheet.id
)

if (
bulk_assignment.sheet_last_modified_date
and sheet_last_modified <= bulk_assignment.sheet_last_modified_date
and not options["force"]
):
raise CommandError(
"Spreadsheet is unchanged since it was last processed (%s, last modified: %s). " # noqa: UP031
"Add the '-f/--force' flag to process it anyway."
% (spreadsheet_repr(spreadsheet), sheet_last_modified.isoformat())
try:
spreadsheet, num_created, num_removed, bulk_assignment_id = fetch_and_process_coupon_assignment(
use_sheet_id=use_sheet_id, value=value, force=options.get("force")
)

coupon_assignment_handler = CouponAssignmentHandler(
spreadsheet_id=spreadsheet.id, bulk_assignment=bulk_assignment
)
(
bulk_assignment,
num_created,
num_removed,
) = coupon_assignment_handler.process_assignment_spreadsheet()
bulk_assignment.sheet_last_modified_date = sheet_last_modified
bulk_assignment.save()
self.stdout.write(
self.style.SUCCESS(
f"Successfully processed coupon assignment sheet ({spreadsheet_repr(spreadsheet)}).\n"
f"{num_created} individual coupon assignment(s) added, {num_removed} deleted (BulkCouponAssignment id: {bulk_assignment.id})."
self.stdout.write(
self.style.SUCCESS(
f"Successfully processed coupon assignment sheet ({value}).\n"
f"{num_created} individual coupon assignment(s) added, {num_removed} deleted "
f"(BulkCouponAssignment id: {bulk_assignment_id})."
)
)
)

except CommandError as e:
raise CommandError(str(e))

except Exception as e:
raise CommandError(f"An unexpected error occurred: {e}")
5 changes: 5 additions & 0 deletions sheets/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@
views.handle_watched_sheet_update,
name="handle-watched-sheet-update",
),
re_path(
r"^api/sheets/process_coupon_sheet_assignment/",
views.ProcessCouponSheetAssignmentView.as_view(),
name="process-coupon-sheet-assignment",
)
]
61 changes: 61 additions & 0 deletions sheets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from django.conf import settings
from django.urls import reverse

from ecommerce.models import BulkCouponAssignment
from mitxpro.utils import matching_item_index
# from sheets.api import ExpandedSheetsClient, get_authorized_pygsheets_client
from sheets.constants import (
ASSIGNMENT_SHEET_PREFIX,
GOOGLE_AUTH_PROVIDER_X509_CERT_URL,
Expand All @@ -25,6 +27,7 @@
WORKSHEET_TYPE_REFUND,
)

from sheets.exceptions import CouponAssignmentError

def generate_google_client_config():
"""Helper method to generate Google client config based on app settings"""
Expand Down Expand Up @@ -630,3 +633,61 @@ def build_drive_file_email_share_request(file_id, email_to_share):
supportsTeamDrives=True,
**added_kwargs,
)


def assign_coupons_from_spreadsheet(use_sheet_id: bool, value: str, force: bool = False):
"""
Fetches and processes a coupon assignment spreadsheet using either the sheet ID or title.
Args:
use_sheet_id (bool): If True, 'value' represents the spreadsheet ID; otherwise, it represents the title.
value (str): The spreadsheet ID or title.
Returns:
dict: A dictionary containing the result of the processing.
"""

if not value:
raise CouponAssignmentError("Spreadsheet identifier (ID or Title) is required.")

from sheets.api import ExpandedSheetsClient, get_authorized_pygsheets_client
from sheets.management.utils import get_assignment_spreadsheet_by_title

pygsheets_client = get_authorized_pygsheets_client()

# Fetch the correct spreadsheet
if use_sheet_id:
spreadsheet = pygsheets_client.open_by_key(value)
else:
spreadsheet = get_assignment_spreadsheet_by_title(pygsheets_client, value)

expanded_sheets_client = ExpandedSheetsClient(pygsheets_client)
metadata = expanded_sheets_client.get_drive_file_metadata(
file_id=spreadsheet.id, fields="modifiedTime"
)
sheet_last_modified = google_date_string_to_datetime(metadata["modifiedTime"])

bulk_assignment, created = BulkCouponAssignment.objects.get_or_create(
assignment_sheet_id=spreadsheet.id
)

if (
bulk_assignment.sheet_last_modified_date
and sheet_last_modified <= bulk_assignment.sheet_last_modified_date
and not force
):
raise CouponAssignmentError(
f"Spreadsheet is unchanged since last processed ({spreadsheet_repr(spreadsheet)}, last modified: {sheet_last_modified.isoformat()})."
)

from sheets.coupon_assign_api import CouponAssignmentHandler
coupon_assignment_handler = CouponAssignmentHandler(
spreadsheet_id=spreadsheet.id, bulk_assignment=bulk_assignment
)

bulk_assignment, num_created, num_removed = coupon_assignment_handler.process_assignment_spreadsheet()
bulk_assignment.sheet_last_modified_date = sheet_last_modified
bulk_assignment.save()

return spreadsheet_repr(spreadsheet), num_created, num_removed, bulk_assignment.id

46 changes: 45 additions & 1 deletion sheets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from google.auth.exceptions import GoogleAuthError
from google_auth_oauthlib.flow import Flow
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from mitxpro.utils import now_in_utc
from sheets import tasks
Expand All @@ -24,7 +26,8 @@
SHEET_TYPE_ENROLL_CHANGE,
)
from sheets.models import GoogleApiAuth, GoogleFileWatch
from sheets.utils import generate_google_client_config
from sheets.utils import generate_google_client_config, assign_coupons_from_spreadsheet
from sheets.exceptions import CouponAssignmentError

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -151,3 +154,44 @@ def handle_watched_sheet_update(request):
tasks.handle_unprocessed_deferral_requests.delay()

return HttpResponse(status=status.HTTP_200_OK)


class ProcessCouponSheetAssignmentView(APIView):
def post(self, request):
"""Handles the assignment of coupons from a sheet (by ID or Title)."""
sheet_identifier_type = request.data.get("sheet_identifier_type")
sheet_identifier_value = request.data.get("sheet_identifier_value")
force = request.data.get("force", True)

if sheet_identifier_type is None or not sheet_identifier_value:
return Response(
{"error": "Both 'sheet_identifier_type' and 'sheet_value' are required."},
status=status.HTTP_400_BAD_REQUEST,
)

# try:
# Call the updated utility function for coupon assignment
spreadsheet_repr, num_created, num_removed, bulk_assignment_id = assign_coupons_from_spreadsheet(
sheet_identifier_type=="id", sheet_identifier_value, force
)

# Return success response with relevant data
return Response(
{
"message": f"Successfully processed coupon assignment sheet ({spreadsheet_repr}).",
"num_created": num_created,
"num_removed": num_removed,
"bulk_assignment_id": bulk_assignment_id,
},
status=status.HTTP_200_OK,
)

# except CouponAssignmentError as e:
# # Handle known errors (like missing or incorrect sheet)
# return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
# except Exception as e:
# # Catch any unexpected errors
# return Response(
# {"error": "An error occurred while processing the coupon sheet."},
# status=status.HTTP_500_INTERNAL_SERVER_ERROR,
# )
77 changes: 77 additions & 0 deletions static/js/components/forms/CouponSheetProcessForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from "react";
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as yup from "yup";

import FormError from "./elements/FormError";

import { SHEET_IDENTIFIER_ID, SHEET_IDENTIFIER_TITLE } from "../../constants";

type CouponSheetProcessFormProps = {
onSubmit: Function,
};

const couponValidations = yup.object().shape({
sheet_identifier_value: yup
.string()
.required("Sheet ID or Title is required")
.matches(
/^[\w\n ]+$/,
"Only letters, numbers, spaces, and underscores allowed"
),
});

export const CouponSheetProcessForm = ({ onSubmit }: CouponSheetProcessFormProps) => {
return (
<Formik
onSubmit={onSubmit}
validationSchema={couponValidations}
initialValues={{
sheet_identifier_value: "",
sheet_identifier_type: SHEET_IDENTIFIER_ID,
}}
render={({ isSubmitting, setFieldValue, values }) => (
<Form className="coupon-form">
<div>
<div className="flex" style={{ marginBottom: "10px" }}>
<label className="flex">
<Field
type="radio"
name="sheet_identifier_type"
value={SHEET_IDENTIFIER_ID}
onClick={() => setFieldValue("sheet_identifier_type", SHEET_IDENTIFIER_ID)}
checked={values.sheet_identifier_type===SHEET_IDENTIFIER_ID}
/>
Use Sheet ID
</label>

<label className="flex">
<Field
type="radio"
name="sheet_identifier_type"
value={SHEET_IDENTIFIER_TITLE}
onClick={() => setFieldValue("sheet_identifier_type", SHEET_IDENTIFIER_TITLE)}
checked={values.sheet_identifier_type===SHEET_IDENTIFIER_TITLE}
/>
Use Sheet Title
</label>
</div>

<div className="block text-area-div">
<label htmlFor="sheet_identifier_value">
{values.sheet_identifier_type===SHEET_IDENTIFIER_ID ? "Sheet ID*" : "Sheet Title*"}
<Field name="sheet_identifier_value" component="textarea" rows="2" cols="20" />
</label>
<ErrorMessage name="sheet_identifier_value" component={FormError} />
</div>
</div>

<div>
<button type="submit" disabled={isSubmitting}>
Process Coupon Sheet
</button>
</div>
</Form>
)}
/>
);
};
3 changes: 3 additions & 0 deletions static/js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const ENROLLABLE_ITEM_ID_SEPARATOR = "+";
export const PRODUCT_TYPE_PROGRAM = "program";
export const PRODUCT_TYPE_COURSERUN = "courserun";

export const SHEET_IDENTIFIER_ID = "id";
export const SHEET_IDENTIFIER_TITLE = "title";

export const GENDER_CHOICES = [
["m", "Male"],
["f", "Female"],
Expand Down
9 changes: 9 additions & 0 deletions static/js/containers/pages/admin/EcommerceAdminPages.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { routes } from "../../../lib/urls";

import CouponCreationPage from "./CreateCouponPage";
import DeactivateCouponPage from "./DeactivateCouponPage";
import ProcessCouponAssignmentSheetPage from "./ProcessCouponAssignmentSheetPage";

const EcommerceAdminIndexPage = () => (
<div className="ecommerce-admin-body">
Expand All @@ -27,6 +28,9 @@ const EcommerceAdminIndexPage = () => (
</Link>
</li>
)}
<li>
<Link to={routes.ecommerceAdmin.processSheets}>Process Coupon Assignment Sheet</Link>
</li>
</ul>
</div>
);
Expand All @@ -49,6 +53,11 @@ const EcommerceAdminPages = () => (
path={routes.ecommerceAdmin.deactivate}
component={DeactivateCouponPage}
/>
<Route
exact
path={routes.ecommerceAdmin.processSheets}
component={ProcessCouponAssignmentSheetPage}
/>
<Redirect to={routes.ecommerceAdmin.index} />
</Switch>
</React.Fragment>
Expand Down
Loading

0 comments on commit acb0aaf

Please sign in to comment.