Skip to content
This repository has been archived by the owner on Nov 5, 2022. It is now read-only.

Update dv360 query to deal with advertiser-level query #62

Merged
merged 5 commits into from
Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/src/json_js.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@JS('JSON')
@JS()
library stringify;

import 'package:js/js.dart';
Expand All @@ -11,5 +11,7 @@ import 'package:js/js.dart';
/// ``` js
/// JSON.stringify()
/// ```
@JS('stringify')
external String stringify(Object obj);
@JS('JSON')
class JsonJS {
external static String stringify(Object obj);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'dart:convert';

import 'proto/insertion_order_query.pb.dart';

class InsertionOrderParser {
/// TODO: parse and handle dv360 public api error.
/// Issue:https://github.com/googleinterns/dv360-excel-plugin/issues/52
class PublicApiParser {
static const _emptyEntry = '';

// Disregarding linter rule to ensure that enum maps are typed.
Expand Down Expand Up @@ -36,10 +38,29 @@ class InsertionOrderParser {
key: (v) => v.name,
value: (v) => v);

/// Parses a json string to [InsertionOrder].
static InsertionOrder parse(String jsonString) {
/// Parses a list of [InsertionOrder] from a [jsonString].
///
/// Returns an empty list if [jsonString] is null or empty.
static List<InsertionOrder> parseInsertionOrders(String jsonString) {
Map<String, dynamic> map = json.decode(jsonString);
return _createInsertionOrder(map);

// If map contains key 'insertionOrders', multiple IOs are returned.
// And if it doesn't, the map itself represents one insertion order.
if (map.containsKey('insertionOrders')) {
return List.from(map['insertionOrders'])
.map((ioMap) => _createInsertionOrder(ioMap))
.toList();
} else {
return [_createInsertionOrder(map)];
}
}

/// Parses the nextPageToken from a [jsonString].
///
/// Returns a empty string if [jsonString] is null or empty.
static String parseNextPageToken(String jsonString) {
Map<String, dynamic> map = json.decode(jsonString);
return map['nextPageToken'] ?? _emptyEntry;
}

/// Creates an [InsertionOrder] instance from [map].
Expand Down
92 changes: 74 additions & 18 deletions lib/src/query_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import 'package:angular/angular.dart';
import 'package:angular_forms/angular_forms.dart';

import 'excel.dart';
import 'insertion_order_parser.dart';
import 'json_js.dart' as json;
import 'public_api_parser.dart';
import 'json_js.dart';
import 'proto/insertion_order_query.pb.dart';
import 'query_service.dart';
import 'reporting_query_parser.dart';
import 'util.dart';

@Component(
selector: 'query',
Expand All @@ -16,31 +17,62 @@ import 'reporting_query_parser.dart';
<input [(ngModel)]="insertionOrderId"
placeholder="Insertion Order ID: 8127549" debugId="io-id-input">
<br>

<input type="radio" [(ngModel)]="advertiserQuery" name="advertiser-query">
<label for="advertiser-query">By advertiser</label><br>
<input type="radio" [(ngModel)]="insertionOrderQuery" name="advertiser-query">
<label for="advertiser-query">By insertion order</label><br>

<input type="checkbox" [(ngModel)]="highlightUnderpacing"
debugId="underpacing" name="underpacing">
<label for="underpacing">Highlight underpacing insertion orders</label><br>

<button (click)="onClick()" debugId="populate-btn">
{{buttonName}}
</button>
''',
providers: [ClassProvider(QueryService), ClassProvider(ExcelDart)],
providers: [
ClassProvider(QueryService),
ClassProvider(ExcelDart),
FORM_PROVIDERS,
],
directives: [coreDirectives, formDirectives],
)
class QueryComponent {
final buttonName = 'populate';
final QueryService _queryService;
final ExcelDart _excel;

QueryType _queryType;

String advertiserId;
String insertionOrderId;

bool highlightUnderpacing = false;

// Radio button states with byAdvertiser selected as default.
RadioButtonState advertiserQuery = RadioButtonState(true, 'byAdvertiser');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the radio button can only take string values :(

Saw other people complain about this too.

RadioButtonState insertionOrderQuery = RadioButtonState(false, 'byIO');

QueryComponent(this._queryService, this._excel);

void onClick() async {
// Determines the query type from radio buttons.
_queryType = advertiserQuery.checked
? QueryType.byAdvertiser
: QueryType.byInsertionOrder;

// Uses DV360 public APIs to fetch entity data.
final insertionOrder = await _queryAndParseInsertionOrderEntityData();
var insertionOrders = await _queryAndParseInsertionOrderEntityData();

// If [_queryType.byAdvertiser], filters out insertion orders
// that are not in-flight.
insertionOrders = _queryType == QueryType.byAdvertiser

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opt: if the filtering happened in place you could avoid re-assigning this variable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dart doesn't seem to have an in-place filter :(

? _filterInFlightInsertionOrders(insertionOrders)
: insertionOrders;

// TODO: update reporting query to deal with multiple IOs.
// Issue: https://github.com/googleinterns/dv360-excel-plugin/issues/61
final insertionOrder = insertionOrders.first;

// Gets dateRange for the active budget segment.
final activeDateRange = insertionOrder.budget.activeBudgetSegment.dateRange;
Expand All @@ -52,16 +84,31 @@ class QueryComponent {
insertionOrder.spent = revenueMap[insertionOrder.insertionOrderId] ?? '';

// Populate the spreadsheet.
await _excel.populate([insertionOrder], highlightUnderpacing);
await _excel.populate(insertionOrders, highlightUnderpacing);
}

/// Fetches insertion order entity related data using DV360 public APIs,
/// then return a parsed [InsertionOrder] instance.
Future<InsertionOrder> _queryAndParseInsertionOrderEntityData() async {
final response =
await _queryService.execDV3Query(advertiserId, insertionOrderId);

return InsertionOrderParser.parse(json.stringify(response));
/// then return a list of parsed [InsertionOrder] instance.
Future<List<InsertionOrder>> _queryAndParseInsertionOrderEntityData() async {
final insertionOrderList = <InsertionOrder>[];
var jsonResponse = '{}';

do {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice do while loop

// Gets the nextPageToken, and having an empty token
// doesn't affect the query.
final nextPageToken = PublicApiParser.parseNextPageToken(jsonResponse);

// Executes dv360 query, parses the response and adds results to the list.
final response = await _queryService.execDV3Query(
_queryType, nextPageToken, advertiserId, insertionOrderId);
jsonResponse = JsonJS.stringify(response);

// Adds all insertion orders in this iteration to the list.
insertionOrderList
.addAll(PublicApiParser.parseInsertionOrders(jsonResponse));
} while (PublicApiParser.parseNextPageToken(jsonResponse).isNotEmpty);

return insertionOrderList;
}

/// Fetches revenue spent data using DBM reporting APIs,
Expand All @@ -72,25 +119,34 @@ class QueryComponent {
final jsonCreateQueryResponse = await _queryService
.execReportingCreateQuery(advertiserId, insertionOrderId, dateRange);
final reportingQueryId = ReportingQueryParser.parseQueryIdFromJsonString(
json.stringify(jsonCreateQueryResponse));
JsonJS.stringify(jsonCreateQueryResponse));

try {
// Uses the queryId to get the report download path.
final jsonGetQueryResponse =
await _queryService.execReportingGetQuery(reportingQueryId);
await _queryService.execReportingGetQuery(reportingQueryId);
final reportingDownloadPath =
ReportingQueryParser.parseDownloadPathFromJsonString(
json.stringify(jsonGetQueryResponse));
ReportingQueryParser.parseDownloadPathFromJsonString(
JsonJS.stringify(jsonGetQueryResponse));

// Downloads the report and parse the response into a revenue map.
final report =
await _queryService.execReportingDownload(reportingDownloadPath);
await _queryService.execReportingDownload(reportingDownloadPath);

return ReportingQueryParser.parseRevenueFromJsonString(report);
} catch(e) {
} catch (e) {
/// TODO: proper error handling.
/// Issue: https://github.com/googleinterns/dv360-excel-plugin/issues/52.
return <String, String>{};
}
}

List<InsertionOrder> _filterInFlightInsertionOrders(
List<InsertionOrder> insertionOrders) {
return insertionOrders
.where((io) =>
io.budget.activeBudgetSegment !=
InsertionOrder_Budget_BudgetSegment())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesnt need to check dates?

Copy link
Contributor Author

@hanj7221 hanj7221 Jul 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the IO is not in-flight, it will have an empty InsertionOrder_Budget_BudgetSegment() for the activeBudgetSegment field.

Date checking has already been done during parsing.

.toList();
}
}
33 changes: 25 additions & 8 deletions lib/src/query_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:fixnum/fixnum.dart';

import 'gapi.dart';
import 'proto/insertion_order_query.pb.dart';
import 'util.dart';

@Injectable()
class QueryService {
Expand All @@ -17,14 +18,13 @@ class QueryService {
///
/// Completer is used here to convert the callback method of [execute] into
/// a future, so that we only proceed when the request finishes executing.
Future<dynamic> execDV3Query(
/// Having an empty [nextPageToken] will not affect the query.
Future<dynamic> execDV3Query(QueryType queryType, String nextPageToken,
String advertiserId, String insertionOrderId) async {
final dv3RequestArgs = RequestArgs(
path: _generateQuery(advertiserId, insertionOrderId), method: 'GET');

final responseCompleter = Completer<dynamic>();
GoogleAPI.client
.request(dv3RequestArgs)
.request(_generateDV3Query(
queryType, nextPageToken, advertiserId, insertionOrderId))
.execute(allowInterop((jsonResp, rawResp) {
responseCompleter.complete(jsonResp);
}));
Expand Down Expand Up @@ -114,8 +114,25 @@ class QueryService {
}

/// Generates query based on user inputs.
static String _generateQuery(String advertiserId, String insertionOrderId) {
return 'https://displayvideo.googleapis.com/v1/advertisers/$advertiserId/'
'insertionOrders/$insertionOrderId';
static RequestArgs _generateDV3Query(QueryType queryType,
String nextPageToken, String advertiserId, String insertionOrderId) {
switch (queryType) {
case QueryType.byAdvertiser:
final filter = 'filter=entityStatus="ENTITY_STATUS_ACTIVE"';
final pageToken = 'pageToken=$nextPageToken';
return RequestArgs(
path: 'https://displayvideo.googleapis.com/v1/advertisers/'
'$advertiserId/insertionOrders?$filter&$pageToken',
method: 'GET');

case QueryType.byInsertionOrder:
return RequestArgs(
path: 'https://displayvideo.googleapis.com/v1/advertisers/'
'$advertiserId/insertionOrders/$insertionOrderId',
method: 'GET');

default:
return RequestArgs();
}
}
}
2 changes: 2 additions & 0 deletions lib/src/util.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:fixnum/fixnum.dart';

enum QueryType { byAdvertiser, byInsertionOrder }

class Util {
/// Convert micros to string in standard unit.
static String convertMicrosToStandardUnitString(Int64 micros) {
Expand Down