Skip to content

Commit

Permalink
Refactor issue model (#201)
Browse files Browse the repository at this point in the history
* Create templating classes for issue model

* Add string representation of sections

* Add return types to base issue

* Migrate bug reporting phase to use the new model

* Remove unused import

* Fix typo

* Update import style

* Rank Labels

* Rename methods

* Lint Fix: const instead of let

* Migrated TeamResponsePhase to use BaseIssue

createIssueModel has been modified to work with baseIssue for the
TeamResponsePhase.

* Empty commit

* Fix Team Response Parsing

* TeamResp. Mostly working, left comment error

* Refactor of Issue Model Creation for TeamResponse

IssueModel Creation for TeamResponse has been refactored to follow the
new mode. The description reading bug has also been fixed as of this
commit.

* Fixed Duplicate Info Display

Duplicate information was not being correctly displayed on toggle. Issue
has now been fixed.

* Fixed Severity Toggling

Severity Toggle Used to reset the issue in some cases. That issue has
been resolved.

* Fix Issue Duplicate Removing Description

* Fix same issue with Assignees

* Lint Fix
  • Loading branch information
ptvrajsk authored and RonakLakhotia committed Sep 25, 2019
1 parent f1a1bb6 commit 36f68d4
Show file tree
Hide file tree
Showing 24 changed files with 653 additions and 39 deletions.
104 changes: 104 additions & 0 deletions src/app/core/models/base-issue.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { TeamResponseTemplate } from './templates/team-response-template.model';
import { Issue } from './issue.model';
import { IssueDispute } from './issue-dispute.model';
import { IssueComment } from './comment.model';
import { TutorModerationIssueTemplate } from './templates/tutor-moderation-issue-template.model';
import { TutorModerationTodoTemplate } from './templates/tutor-moderation-todo-template.model';
import { Team } from './team.model';
import { TesterResponse } from './tester-response.model';
import { TesterResponseTemplate } from './templates/tester-response-template.model';
import { GithubIssue, GithubLabel } from './github-issue.model';
import * as moment from 'moment';
import { GithubComment } from './github-comment.model';
import { DataService } from '../services/data.service';

export class BaseIssue implements Issue {

/** Basic Fields */
readonly id: number;
readonly created_at: string;
title: string;
description: string;

/** Fields derived from Labels */
severity: string;
type: string;
responseTag?: string;
duplicated?: boolean;
status?: string;
pending?: string;
unsure?: boolean;
teamAssigned?: Team;

/** Depending on the phase, assignees attribute can be derived from Github's assignee feature OR from the Github's issue description */
assignees?: string[];

/** Fields derived from parsing of Github's issue description */
duplicateOf?: number;
todoList?: string[];
teamResponse?: string;
tutorResponse?: string;
testerResponses?: TesterResponse[];
issueComment?: IssueComment; // Issue comment is used for Tutor Response and Tester Response
issueDisputes?: IssueDispute[];

protected constructor(githubIssue: GithubIssue) {
/** Basic Fields */
this.id = +githubIssue.number;
this.created_at = moment(githubIssue.created_at).format('lll');
this.title = githubIssue.title;
this.description = githubIssue.body;

/** Fields derived from Labels */
this.severity = githubIssue.findLabel(GithubLabel.LABELS.severity);
this.type = githubIssue.findLabel(GithubLabel.LABELS.type);
this.responseTag = githubIssue.findLabel(GithubLabel.LABELS.response);
this.duplicated = !!githubIssue.findLabel(GithubLabel.LABELS.duplicated, false);
this.status = githubIssue.findLabel(GithubLabel.LABELS.status);
this.pending = githubIssue.findLabel(GithubLabel.LABELS.pending);
}

private static constructTeamData(githubIssue: GithubIssue, dataService: DataService): Team {
const teamId = githubIssue.findLabel(GithubLabel.LABELS.tutorial).concat('-').concat(githubIssue.findLabel(GithubLabel.LABELS.team));
return dataService.getTeam(teamId);
}

public static createPhaseBugReportingIssue(githubIssue: GithubIssue): BaseIssue {
return new BaseIssue(githubIssue);
}

public static createPhaseTeamResponseIssue(githubIssue: GithubIssue, githubComments: GithubComment[]
, teamData: Team): Issue {
const issue = new BaseIssue(githubIssue);
const template = new TeamResponseTemplate(githubComments);

issue.teamAssigned = teamData;
issue.issueComment = template.comment;
issue.teamResponse = template.teamResponse !== undefined ? template.teamResponse.content : undefined;
issue.duplicateOf = template.duplicateOf !== undefined ? template.duplicateOf.issueNumber : undefined;
issue.duplicated = issue.duplicateOf !== undefined && issue.duplicateOf !== null;
issue.assignees = githubIssue.assignees.map(assignee => assignee.login);

return issue;
}

public static createPhaseTesterResponseIssue(githubIssue: GithubIssue, githubComments: GithubComment[]): Issue {
const issue = new BaseIssue(githubIssue);
const template = new TesterResponseTemplate(githubComments);
issue.teamResponse = template.teamResponse.content;
issue.testerResponses = template.testerResponse.testerResponses;
return issue;
}

public static createPhaseModerationIssue(githubIssue: GithubIssue, githubComments: GithubComment[]): Issue {
const issue = new BaseIssue(githubIssue);
const issueTemplate = new TutorModerationIssueTemplate(githubIssue);
const todoTemplate = new TutorModerationTodoTemplate(githubComments);

issue.description = issueTemplate.description.content;
issue.teamResponse = issueTemplate.teamResponse.content;
issue.issueDisputes = issueTemplate.dispute.disputes;
issue.todoList = todoTemplate.moderation.todoList;
return issue;
}
}
16 changes: 16 additions & 0 deletions src/app/core/models/github-comment.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface GithubComment {
author_association: string;
body: string;
created_at: string;
html_url: string;
id: number;
issue_url: string;
updated_at: string;
url: string; // api url
user: {
login: string,
id: number,
avatar_url: string,
url: string,
};
}
113 changes: 113 additions & 0 deletions src/app/core/models/github-issue.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
export class GithubLabel {
static readonly LABEL_ORDER = {
severity: { Low: 0, Medium: 1, High: 2 },
type: { DocumentationBug: 0, FunctionalityBug: 1 },
};
static readonly LABELS = {
severity: 'severity',
type: 'type',
response: 'response',
duplicated: 'duplicated',
status: 'status',
unsure: 'unsure',
pending: 'pending',
team: 'team',
tutorial: 'tutorial'
};

color: string;
id: number;
name: string;
url: string;

constructor(githubLabels: {}) {
Object.assign(this, githubLabels);
}

getCategory(): string {
if (this.isCategorical()) {
return this.name.split('.')[0];
} else {
return this.name;
}
}

getValue(): string {
if (this.isCategorical()) {
return this.name.split('.')[1];
} else {
return this.name;
}
}

isCategorical(): boolean {
const regex = /^\S+.\S+$/;
return regex.test(this.name);
}
}

export class GithubIssue {
id: number; // Github's backend's id
number: number; // Issue's display id
assignees: Array<{
id: number,
login: string,
url: string,
}>;
body: string;
created_at: string;
labels: Array<GithubLabel>;
state: string;
title: string;
updated_at: string;
url: string;
user: { // Author of the issue
login: string,
id: number,
avatar_url: string,
url: string,
};

constructor(githubIssue: {}) {
Object.assign(this, githubIssue);
this.labels = [];
for (const label of githubIssue['labels']) {
this.labels.push(new GithubLabel(label));
}
}

/**
*
* @param name Depending on the isCategorical flag, this name either refers to the category name of label or the exact name of label.
* @param isCategorical Whether the label is categorical.
*/
findLabel(name: string, isCategorical: boolean = true): string {
if (!isCategorical) {
const label = this.labels.find(l => (!l.isCategorical() && l.name === name));
return label ? label.getValue() : undefined;
}

// Find labels with the same category name as what is specified in the parameter.
const labels = this.labels.filter(l => (l.isCategorical() && l.getCategory() === name));
if (labels.length === 0) {
return undefined;
} else if (labels.length === 1) {
return labels[0].getValue();
} else {
// If Label order is not specified, return the first label value else
// If Label order is specified, return the highest ranking label value
if (!GithubLabel.LABEL_ORDER[name]) {
return labels[0].getValue();
} else {
const order = GithubLabel.LABEL_ORDER[name];
return labels.reduce((result, currLabel) => {
return order[currLabel.getValue()] > order[result.getValue()] ? currLabel : result;
}).getValue();
}
}
}

findTeamId(): string {
return `${this.findLabel('team')}.${this.findLabel('tutorial')}`;
}
}
4 changes: 2 additions & 2 deletions src/app/core/models/issue.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Team} from './team.model';
import { Team } from './team.model';
import { TesterResponse } from './tester-response.model';
import { IssueComment, IssueComments } from './comment.model';
import { IssueComment } from './comment.model';
import { IssueDispute } from './issue-dispute.model';

export interface Issue {
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/models/team.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {User} from './user.model';
import { User } from './user.model';

export interface Team {
id: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Section, SectionalDependency } from './section.model';

export class DuplicateOfSection extends Section {
private readonly duplicateOfRegex = /Duplicate of\s*#(\d+)/i;
issueNumber: number;

constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) {
super(sectionalDependency, unprocessedContent);
if (!this.parseError) {
this.issueNumber = this.parseDuplicateOfValue(this.content);
}
}

private parseDuplicateOfValue(toParse): number {
const result = this.duplicateOfRegex.exec(toParse);
return result ? +result[1] : null;
}

toString(): string {
let toString = '';
toString += `${this.header}\n`;
toString += this.parseError ? '--' : `Duplicate of ${this.issueNumber}\n`;
return toString;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IssueDispute } from '../../issue-dispute.model';
import { Section, SectionalDependency } from './section.model';

export class IssueDisputeSection extends Section {
disputes: IssueDispute[];

constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) {
super(sectionalDependency, unprocessedContent);
if (!this.parseError) {
let matches;
const regex = /#{2} *:question: *(.*)[\r\n]*([\s\S]*?(?=-{19}))/gi;
while (matches = regex.exec(this.content)) {
if (matches) {
const [regexString, title, description] = matches;
this.disputes.push(new IssueDispute(title, description.trim()));
}
}
}
}

toString(): string {
let toString = '';
toString += `${this.header.toString()}\n`;
for (const dispute of this.disputes) {
toString += `${dispute.toString()}\n`;
}
return toString;
}
}
38 changes: 38 additions & 0 deletions src/app/core/models/templates/sections/moderation-section.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IssueDispute } from '../../issue-dispute.model';
import { Section, SectionalDependency } from './section.model';

export class ModerationSection extends Section {
disputesToResolve: IssueDispute[];

constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) {
super(sectionalDependency, unprocessedContent);
if (!this.parseError) {
let matches;
const regex = /#{2} *:question: *(.*)[\n\r]*(.*)[\n\r]*([\s\S]*?(?=-{19}))/gi;
while (matches = regex.exec(this.content)) {
if (matches) {
const [regexString, title, todo, tutorResponse] = matches;
const description = `${todo}\n${tutorResponse}`;

const newDispute = new IssueDispute(title, description);
newDispute.todo = todo;
newDispute.tutorResponse = tutorResponse.trim();
this.disputesToResolve.push(newDispute);
}
}
}
}

get todoList(): string[] {
return this.disputesToResolve.map(e => e.todo);
}

toString(): string {
let toString = '';
toString += `${this.header.toString()}\n`;
for (const dispute of this.disputesToResolve) {
toString += `${dispute.toTutorResponseString()}\n`;
}
return toString;
}
}
39 changes: 39 additions & 0 deletions src/app/core/models/templates/sections/section.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* A SectionalDependency defines a format that is needed to create a successful Section in a template.
* It will require the Section's header to be defined and the other headers that are present in the template.
*
* Reason for the dependencies on other headers: We need them to create a regex expression that is capable of parsing the current
* section out of the string.
*/
import { Header } from '../template.model';

export interface SectionalDependency {
sectionHeader: Header;
remainingTemplateHeaders: Header[];
}

export class Section {
header: Header;
sectionRegex: RegExp;
content: string;
parseError: string;

/**
*
* @param sectionalDependency The dependency that is need to create a section's regex
* @param unprocessedContent The string that stores the section's amongst other things
*/
constructor(sectionalDependency: SectionalDependency, unprocessedContent: string) {
this.header = sectionalDependency.sectionHeader;
this.sectionRegex = new RegExp(`(${this.header})\\s+([\\s\\S]*?)(?=${sectionalDependency.remainingTemplateHeaders.join('|')}|$)`, 'i');
const matches = this.sectionRegex.exec(unprocessedContent);
if (matches) {
const [originalString, header, description] = matches;
this.content = description.trim();
this.parseError = null;
} else {
this.content = null;
this.parseError = `Unable to extract ${this.header.name} Section`;
}
}
}
Loading

0 comments on commit 36f68d4

Please sign in to comment.