Refactor project structure and add new features
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
{
|
||||
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
|
||||
"id": "494944",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issue/494944",
|
||||
"key": "WPCMU-9082",
|
||||
"fields": {
|
||||
"customfield_19200": null,
|
||||
"customfield_18113": null,
|
||||
"customfield_46700": null,
|
||||
"customfield_44400": null,
|
||||
"customfield_15400": null,
|
||||
"customfield_17700": null,
|
||||
"customfield_38400": null,
|
||||
"customfield_18115": null,
|
||||
"resolution": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/resolution/6",
|
||||
"id": "6",
|
||||
"description": "the task is done",
|
||||
"name": "Done"
|
||||
},
|
||||
"customfield_10500": null,
|
||||
"customfield_36104": null,
|
||||
"customfield_36103": null,
|
||||
"customfield_36102": null,
|
||||
"customfield_24001": null,
|
||||
"customfield_45602": null,
|
||||
"customfield_22601": null,
|
||||
"customfield_47900": null,
|
||||
"lastViewed": null,
|
||||
"customfield_45501": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/20611",
|
||||
"value": "Desktop",
|
||||
"id": "20611",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_47001": null,
|
||||
"customfield_16600": null,
|
||||
"customfield_14300": null,
|
||||
"customfield_12000": "0|i0jh73:",
|
||||
"customfield_39500": null,
|
||||
"customfield_14301": null,
|
||||
"customfield_47000": null,
|
||||
"customfield_14304": null,
|
||||
"customfield_32305": null,
|
||||
"customfield_14302": null,
|
||||
"customfield_16601": null,
|
||||
"customfield_18107": null,
|
||||
"customfield_14303": null,
|
||||
"labels": [],
|
||||
"aggregatetimeoriginalestimate": null,
|
||||
"issuelinks": [
|
||||
{
|
||||
"id": "485247",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issueLink/485247",
|
||||
"type": {
|
||||
"id": "10010",
|
||||
"name": "1 Зависит от таска",
|
||||
"inward": "Parent task",
|
||||
"outward": "Child task",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issueLinkType/10010"
|
||||
},
|
||||
"inwardIssue": {
|
||||
"id": "494942",
|
||||
"key": "WPCMU-9081",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issue/494942",
|
||||
"fields": {
|
||||
"summary": "SLOTS-2",
|
||||
"status": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/status/6",
|
||||
"description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.",
|
||||
"iconUrl": "https://jira.crazypanda.ru/images/icons/statuses/closed.png",
|
||||
"name": "Closed",
|
||||
"id": "6",
|
||||
"statusCategory": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/statuscategory/3",
|
||||
"id": 3,
|
||||
"key": "done",
|
||||
"colorName": "green",
|
||||
"name": "Done"
|
||||
}
|
||||
},
|
||||
"issuetype": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issuetype/7",
|
||||
"id": "7",
|
||||
"description": "Created by Jira Software - do not edit or delete. Issue type for a user story.",
|
||||
"iconUrl": "https://jira.crazypanda.ru/secure/viewavatar?size=xsmall&avatarId=16435&avatarType=issuetype",
|
||||
"name": "Story",
|
||||
"subtask": false,
|
||||
"avatarId": 16435
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "502299",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issueLink/502299",
|
||||
"type": {
|
||||
"id": "11620",
|
||||
"name": "Mention",
|
||||
"inward": "is mentioned by",
|
||||
"outward": "mentions",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issueLinkType/11620"
|
||||
},
|
||||
"outwardIssue": {
|
||||
"id": "503922",
|
||||
"key": "WPCMU-9682",
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issue/503922",
|
||||
"fields": {
|
||||
"summary": "LOC: SLOTS-2",
|
||||
"status": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/status/5",
|
||||
"description": "Story has been released less then 1.5m ago.",
|
||||
"iconUrl": "https://jira.crazypanda.ru/images/icons/statuses/generic.png",
|
||||
"name": "Resolved",
|
||||
"id": "5",
|
||||
"statusCategory": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/statuscategory/3",
|
||||
"id": 3,
|
||||
"key": "done",
|
||||
"colorName": "green",
|
||||
"name": "Done"
|
||||
}
|
||||
},
|
||||
"issuetype": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issuetype/3",
|
||||
"id": "3",
|
||||
"description": "A task that needs to be done.",
|
||||
"iconUrl": "https://jira.crazypanda.ru/secure/viewavatar?size=xsmall&avatarId=16438&avatarType=issuetype",
|
||||
"name": "Task",
|
||||
"subtask": false,
|
||||
"avatarId": 16438
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=s.zaigraev",
|
||||
"name": "s.zaigraev",
|
||||
"key": "s.zaigraev",
|
||||
"emailAddress": "s.zaigraev@crazypandagames.com",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://jira.crazypanda.ru/secure/useravatar?ownerId=s.zaigraev&avatarId=35220",
|
||||
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&ownerId=s.zaigraev&avatarId=35220",
|
||||
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&ownerId=s.zaigraev&avatarId=35220",
|
||||
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&ownerId=s.zaigraev&avatarId=35220"
|
||||
},
|
||||
"displayName": "Заиграев Сергей",
|
||||
"active": true,
|
||||
"timeZone": "Europe/Moscow"
|
||||
},
|
||||
"customfield_46702": null,
|
||||
"customfield_46701": null,
|
||||
"customfield_42901": null,
|
||||
"customfield_42900": null,
|
||||
"customfield_23601": null,
|
||||
"customfield_19300": null,
|
||||
"customfield_44300": null,
|
||||
"components": [],
|
||||
"customfield_44301": null,
|
||||
"customfield_46600": null,
|
||||
"customfield_17003": null,
|
||||
"customfield_17002": null,
|
||||
"customfield_17001": null,
|
||||
"customfield_17000": null,
|
||||
"customfield_15500": null,
|
||||
"customfield_17004": null,
|
||||
"customfield_15503": null,
|
||||
"customfield_15501": null,
|
||||
"customfield_45502": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/20613",
|
||||
"value": "2",
|
||||
"id": "20613",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_45400": null,
|
||||
"customfield_43100": null,
|
||||
"customfield_47701": null,
|
||||
"customfield_43101": null,
|
||||
"customfield_18201": null,
|
||||
"customfield_43102": null,
|
||||
"customfield_43103": null,
|
||||
"subtasks": [],
|
||||
"customfield_39400": null,
|
||||
"customfield_14400": null,
|
||||
"reporter": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=n.andreenko",
|
||||
"name": "n.andreenko",
|
||||
"key": "JIRAUSER64600",
|
||||
"emailAddress": "n.andreenko@crazypandagames.com",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://jira.crazypanda.ru/secure/useravatar?avatarId=27235",
|
||||
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&avatarId=27235",
|
||||
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&avatarId=27235",
|
||||
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&avatarId=27235"
|
||||
},
|
||||
"displayName": "Андреенко Наталья",
|
||||
"active": true,
|
||||
"timeZone": "Europe/Moscow"
|
||||
},
|
||||
"customfield_14401": null,
|
||||
"customfield_16700": null,
|
||||
"customfield_40507": null,
|
||||
"progress": {
|
||||
"progress": 0,
|
||||
"total": 0
|
||||
},
|
||||
"votes": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issue/WPCMU-9082/votes",
|
||||
"votes": 0,
|
||||
"hasVoted": false
|
||||
},
|
||||
"worklog": {
|
||||
"startAt": 0,
|
||||
"maxResults": 20,
|
||||
"total": 0,
|
||||
"worklogs": []
|
||||
},
|
||||
"customfield_44303": null,
|
||||
"customfield_42802": null,
|
||||
"customfield_44305": null,
|
||||
"customfield_42801": null,
|
||||
"customfield_46500": null,
|
||||
"issuetype": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issuetype/3",
|
||||
"id": "3",
|
||||
"description": "A task that needs to be done.",
|
||||
"iconUrl": "https://jira.crazypanda.ru/secure/viewavatar?size=xsmall&avatarId=16438&avatarType=issuetype",
|
||||
"name": "Task",
|
||||
"subtask": false,
|
||||
"avatarId": 16438
|
||||
},
|
||||
"customfield_19400": null,
|
||||
"customfield_46502": null,
|
||||
"customfield_19401": null,
|
||||
"customfield_46501": null,
|
||||
"project": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/project/15550",
|
||||
"id": "15550",
|
||||
"key": "WPCMU",
|
||||
"name": "World Poker Club Unity Mobile",
|
||||
"projectTypeKey": "software",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://jira.crazypanda.ru/secure/projectavatar?avatarId=27223",
|
||||
"24x24": "https://jira.crazypanda.ru/secure/projectavatar?size=small&avatarId=27223",
|
||||
"16x16": "https://jira.crazypanda.ru/secure/projectavatar?size=xsmall&avatarId=27223",
|
||||
"32x32": "https://jira.crazypanda.ru/secure/projectavatar?size=medium&avatarId=27223"
|
||||
}
|
||||
},
|
||||
"customfield_15602": null,
|
||||
"customfield_17900": null,
|
||||
"customfield_15600": null,
|
||||
"customfield_15601": null,
|
||||
"customfield_34401": null,
|
||||
"customfield_16804": null,
|
||||
"customfield_16803": null,
|
||||
"resolutiondate": "2025-08-29T17:43:43.998+0300",
|
||||
"customfield_41600": null,
|
||||
"customfield_43901": null,
|
||||
"customfield_22801": null,
|
||||
"customfield_43104": null,
|
||||
"customfield_43105": [],
|
||||
"customfield_43106": null,
|
||||
"customfield_43107": null,
|
||||
"customfield_45300": null,
|
||||
"watches": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issue/WPCMU-9082/watchers",
|
||||
"watchCount": 0,
|
||||
"isWatching": false
|
||||
},
|
||||
"customfield_18300": null,
|
||||
"customfield_18301": null,
|
||||
"customfield_16002": null,
|
||||
"customfield_16001": null,
|
||||
"customfield_16000": null,
|
||||
"customfield_49102": 8135645407,
|
||||
"customfield_39300": null,
|
||||
"customfield_49103": "shortFormatDuration({issue.cf49102})",
|
||||
"customfield_49100": "2025-05-27T13:49:38.366+0300",
|
||||
"customfield_16005": 100.0,
|
||||
"customfield_16004": null,
|
||||
"customfield_16003": null,
|
||||
"customfield_18302": null,
|
||||
"customfield_16802": null,
|
||||
"customfield_16801": null,
|
||||
"customfield_14500": null,
|
||||
"customfield_46507": null,
|
||||
"customfield_46509": null,
|
||||
"customfield_46504": null,
|
||||
"customfield_46503": null,
|
||||
"updated": "2025-10-07T17:12:22.404+0300",
|
||||
"customfield_23401": null,
|
||||
"customfield_46506": null,
|
||||
"customfield_46505": null,
|
||||
"customfield_23203": null,
|
||||
"customfield_46400": null,
|
||||
"customfield_23202": null,
|
||||
"customfield_23201": null,
|
||||
"customfield_19500": null,
|
||||
"customfield_17200": null,
|
||||
"timeoriginalestimate": null,
|
||||
"description": "Ссылка на локу: WPCMU-9682",
|
||||
"customfield_11100": null,
|
||||
"customfield_15701": null,
|
||||
"timetracking": {},
|
||||
"customfield_15700": null,
|
||||
"customfield_10005": null,
|
||||
"customfield_41500": null,
|
||||
"summary": "GD: SLOTS-2",
|
||||
"customfield_47500": [],
|
||||
"customfield_47501": [],
|
||||
"customfield_16100": null,
|
||||
"customfield_45200": null,
|
||||
"customfield_49000": null,
|
||||
"customfield_18401": null,
|
||||
"customfield_18402": null,
|
||||
"customfield_16900": null,
|
||||
"customfield_18403": null,
|
||||
"customfield_18404": null,
|
||||
"customfield_14600": null,
|
||||
"duedate": "2025-06-06",
|
||||
"customfield_44902": null,
|
||||
"comment": {
|
||||
"comments": [
|
||||
{
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/issue/494944/comment/598612",
|
||||
"id": "598612",
|
||||
"author": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=s.zaigraev",
|
||||
"name": "s.zaigraev",
|
||||
"key": "s.zaigraev",
|
||||
"emailAddress": "s.zaigraev@crazypandagames.com",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://jira.crazypanda.ru/secure/useravatar?ownerId=s.zaigraev&avatarId=35220",
|
||||
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&ownerId=s.zaigraev&avatarId=35220",
|
||||
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&ownerId=s.zaigraev&avatarId=35220",
|
||||
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&ownerId=s.zaigraev&avatarId=35220"
|
||||
},
|
||||
"displayName": "Заиграев Сергей",
|
||||
"active": true,
|
||||
"timeZone": "Europe/Moscow"
|
||||
},
|
||||
"body": "Наборы слотов: https://confluence.crazypanda.ru/pages/viewpage.action?pageId=1458307128\r\nСписок механик (см дочерние страницы): https://confluence.crazypanda.ru/pages/viewpage.action?pageId=1352728643",
|
||||
"updateAuthor": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=s.zaigraev",
|
||||
"name": "s.zaigraev",
|
||||
"key": "s.zaigraev",
|
||||
"emailAddress": "s.zaigraev@crazypandagames.com",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://jira.crazypanda.ru/secure/useravatar?ownerId=s.zaigraev&avatarId=35220",
|
||||
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&ownerId=s.zaigraev&avatarId=35220",
|
||||
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&ownerId=s.zaigraev&avatarId=35220",
|
||||
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&ownerId=s.zaigraev&avatarId=35220"
|
||||
},
|
||||
"displayName": "Заиграев Сергей",
|
||||
"active": true,
|
||||
"timeZone": "Europe/Moscow"
|
||||
},
|
||||
"created": "2025-06-06T21:41:52.591+0300",
|
||||
"updated": "2025-06-06T21:41:52.591+0300"
|
||||
}
|
||||
],
|
||||
"maxResults": 1,
|
||||
"total": 1,
|
||||
"startAt": 0
|
||||
},
|
||||
"customfield_44903": null,
|
||||
"customfield_23301": null,
|
||||
"customfield_17300": null,
|
||||
"customfield_46300": null,
|
||||
"customfield_44001": null,
|
||||
"customfield_17302": null,
|
||||
"customfield_17301": null,
|
||||
"fixVersions": [],
|
||||
"customfield_19600": null,
|
||||
"customfield_38800": null,
|
||||
"customfield_13501": null,
|
||||
"customfield_14702": null,
|
||||
"customfield_14703": null,
|
||||
"customfield_41400": null,
|
||||
"customfield_43700": null,
|
||||
"customfield_45103": null,
|
||||
"customfield_47400": null,
|
||||
"customfield_45100": null,
|
||||
"customfield_10100": "9223372036854775807",
|
||||
"customfield_14700": null,
|
||||
"customfield_12400": null,
|
||||
"customfield_14701": null,
|
||||
"customfield_15902": null,
|
||||
"timeestimate": null,
|
||||
"versions": [],
|
||||
"customfield_40202": null,
|
||||
"customfield_40200": null,
|
||||
"customfield_42500": "{}",
|
||||
"customfield_40201": null,
|
||||
"status": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/status/5",
|
||||
"description": "Story has been released less then 1.5m ago.",
|
||||
"iconUrl": "https://jira.crazypanda.ru/images/icons/statuses/generic.png",
|
||||
"name": "Resolved",
|
||||
"id": "5",
|
||||
"statusCategory": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/statuscategory/3",
|
||||
"id": 3,
|
||||
"key": "done",
|
||||
"colorName": "green",
|
||||
"name": "Done"
|
||||
}
|
||||
},
|
||||
"customfield_40100": null,
|
||||
"customfield_15100": null,
|
||||
"customfield_46200": null,
|
||||
"customfield_15101": null,
|
||||
"customfield_15102": null,
|
||||
"customfield_17400": null,
|
||||
"customfield_38700": null,
|
||||
"customfield_19700": "{}",
|
||||
"aggregatetimeestimate": null,
|
||||
"customfield_14806": null,
|
||||
"customfield_45902": null,
|
||||
"customfield_45901": null,
|
||||
"customfield_47302": null,
|
||||
"creator": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/user?username=n.andreenko",
|
||||
"name": "n.andreenko",
|
||||
"key": "JIRAUSER64600",
|
||||
"emailAddress": "n.andreenko@crazypandagames.com",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://jira.crazypanda.ru/secure/useravatar?avatarId=27235",
|
||||
"24x24": "https://jira.crazypanda.ru/secure/useravatar?size=small&avatarId=27235",
|
||||
"16x16": "https://jira.crazypanda.ru/secure/useravatar?size=xsmall&avatarId=27235",
|
||||
"32x32": "https://jira.crazypanda.ru/secure/useravatar?size=medium&avatarId=27235"
|
||||
},
|
||||
"displayName": "Андреенко Наталья",
|
||||
"active": true,
|
||||
"timeZone": "Europe/Moscow"
|
||||
},
|
||||
"customfield_41200": null,
|
||||
"customfield_47300": null,
|
||||
"customfield_45000": null,
|
||||
"customfield_47301": "<a href=\"https://confluence.crazypanda.ru/x/CgBgO\">https://confluence.crazypanda.ru/x/CgBgO</a>",
|
||||
"customfield_16300": null,
|
||||
"aggregateprogress": {
|
||||
"progress": 0,
|
||||
"total": 0
|
||||
},
|
||||
"customfield_39800": null,
|
||||
"customfield_18600": null,
|
||||
"customfield_10200": [
|
||||
"com.atlassian.greenhopper.service.sprint.Sprint@1f8ba4cd[id=673,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 188,startDate=2025-05-27T12:57:00.000+03:00,endDate=2025-06-10T12:57:00.000+03:00,completeDate=2025-06-17T11:28:01.892+03:00,activatedDate=2025-05-27T12:57:24.826+03:00,sequence=674,goal=,autoStartStop=false]",
|
||||
"com.atlassian.greenhopper.service.sprint.Sprint@4e0528d1[id=688,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 189,startDate=2025-06-16T11:29:00.000+03:00,endDate=2025-06-27T11:29:00.000+03:00,completeDate=2025-06-30T11:08:30.366+03:00,activatedDate=2025-06-17T11:30:29.377+03:00,sequence=688,goal=,autoStartStop=false]",
|
||||
"com.atlassian.greenhopper.service.sprint.Sprint@68f8b40a[id=692,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 190,startDate=2025-06-30T11:07:00.000+03:00,endDate=2025-07-11T11:07:00.000+03:00,completeDate=2025-07-14T10:48:35.813+03:00,activatedDate=2025-06-30T11:08:56.638+03:00,sequence=692,goal=,autoStartStop=false]",
|
||||
"com.atlassian.greenhopper.service.sprint.Sprint@833683d[id=699,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 191,startDate=2025-07-14T12:06:00.000+03:00,endDate=2025-07-25T12:06:00.000+03:00,completeDate=2025-07-28T11:15:06.279+03:00,activatedDate=2025-07-14T10:48:57.081+03:00,sequence=699,goal=,autoStartStop=false]",
|
||||
"com.atlassian.greenhopper.service.sprint.Sprint@196c70b9[id=714,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 192,startDate=2025-07-28T11:14:00.000+03:00,endDate=2025-08-08T11:14:00.000+03:00,completeDate=2025-08-11T10:27:17.098+03:00,activatedDate=2025-07-28T11:16:18.567+03:00,sequence=714,goal=,autoStartStop=false]",
|
||||
"com.atlassian.greenhopper.service.sprint.Sprint@d6ac476[id=720,rapidViewId=506,state=CLOSED,name=MOB -Team 1- Sprint 193,startDate=2025-08-11T10:25:00.000+03:00,endDate=2025-08-22T10:25:00.000+03:00,completeDate=2025-08-25T17:31:38.356+03:00,activatedDate=2025-08-11T10:28:03.549+03:00,sequence=720,goal=,autoStartStop=false]"
|
||||
],
|
||||
"customfield_18601": null,
|
||||
"customfield_40900": [],
|
||||
"customfield_19002": null,
|
||||
"customfield_19003": null,
|
||||
"customfield_19004": null,
|
||||
"customfield_40000": null,
|
||||
"customfield_46100": null,
|
||||
"customfield_40001": null,
|
||||
"customfield_19000": null,
|
||||
"customfield_46102": null,
|
||||
"timespent": null,
|
||||
"customfield_19001": null,
|
||||
"customfield_46101": null,
|
||||
"customfield_15200": null,
|
||||
"customfield_17500": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/13601",
|
||||
"value": "No",
|
||||
"id": "13601",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_38601": null,
|
||||
"aggregatetimespent": null,
|
||||
"customfield_13700": null,
|
||||
"customfield_19800": null,
|
||||
"customfield_10302": null,
|
||||
"customfield_43505": null,
|
||||
"workratio": -1,
|
||||
"customfield_43506": null,
|
||||
"customfield_43507": null,
|
||||
"customfield_45800": null,
|
||||
"customfield_43500": null,
|
||||
"customfield_45801": null,
|
||||
"customfield_43503": null,
|
||||
"customfield_49500": null,
|
||||
"customfield_47202": "<span style=\"color: #ff6600;\">Привет, ты редактируешь Request во внутренние сервисы.<br>\r\nСвободно оставляй тут вопросы, просьбу помощи, баг, запрос на фичу. <br>\r\nПодробное описание, как это работает смотри <a href=\"https://confluence.crazypanda.ru/pages/viewpage.action?pageId=945815562\">тут</a></span></p>",
|
||||
"created": "2025-05-19T13:10:05.955+0300",
|
||||
"customfield_47201": null,
|
||||
"customfield_16402": null,
|
||||
"customfield_16401": null,
|
||||
"customfield_16400": null,
|
||||
"customfield_39700": null,
|
||||
"customfield_10300": null,
|
||||
"customfield_10301": null,
|
||||
"customfield_18701": null,
|
||||
"customfield_13800": null,
|
||||
"customfield_42303": null,
|
||||
"customfield_44600": null,
|
||||
"customfield_44500": null,
|
||||
"customfield_46001": null,
|
||||
"customfield_46000": null,
|
||||
"customfield_46003": null,
|
||||
"customfield_19100": null,
|
||||
"customfield_46002": null,
|
||||
"customfield_17601": null,
|
||||
"customfield_17600": null,
|
||||
"customfield_13000": null,
|
||||
"customfield_38500": null,
|
||||
"customfield_15300": null,
|
||||
"customfield_38501": null,
|
||||
"customfield_38502": null,
|
||||
"customfield_19900": "{}",
|
||||
"attachment": [],
|
||||
"customfield_45700": null,
|
||||
"customfield_41900": null,
|
||||
"customfield_49405": null,
|
||||
"customfield_46810": null,
|
||||
"customfield_43300": null,
|
||||
"customfield_49403": null,
|
||||
"customfield_24902": null,
|
||||
"customfield_24903": null,
|
||||
"customfield_47100": null,
|
||||
"customfield_41000": null,
|
||||
"customfield_49400": null,
|
||||
"customfield_16501": null,
|
||||
"customfield_18801": null,
|
||||
"customfield_16500": null,
|
||||
"customfield_18802": null,
|
||||
"customfield_18800": null,
|
||||
"customfield_13900": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11301",
|
||||
"value": "нет",
|
||||
"id": "11301",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_13902": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11307",
|
||||
"value": "нет",
|
||||
"id": "11307",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_13901": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11304",
|
||||
"value": "нет",
|
||||
"id": "11304",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_46809": null,
|
||||
"customfield_13904": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11314",
|
||||
"value": "Не требуется",
|
||||
"id": "11314",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_46808": null,
|
||||
"customfield_13903": {
|
||||
"self": "https://jira.crazypanda.ru/rest/api/2/customFieldOption/11310",
|
||||
"value": "нет",
|
||||
"id": "11310",
|
||||
"disabled": false
|
||||
},
|
||||
"customfield_46804": null,
|
||||
"customfield_46807": null,
|
||||
"customfield_46806": null,
|
||||
"customfield_42202": null,
|
||||
"customfield_46801": null,
|
||||
"customfield_46800": null,
|
||||
"customfield_46803": null,
|
||||
"customfield_46802": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
# Integration Plan: Parent Story and Story Points for Global Tasks
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the plan to enhance the `fetch_all_tasks_with_due_dates()` method in `client_jira.py` to include parent story information and story points for each task. This enhancement applies **only** to global tasks that appear in the Tasks report table, not to individual assignee-specific tasks. The enhancement will include a caching mechanism to optimize performance when multiple tasks belong to the same parent story.
|
||||
|
||||
## Scope Clarification
|
||||
|
||||
**Included in this enhancement:**
|
||||
- Global tasks fetched via `fetch_all_tasks_with_due_dates()`
|
||||
- Tasks that appear in the Tasks report table
|
||||
- Parent story key and storypoints extraction
|
||||
|
||||
**Excluded from this enhancement:**
|
||||
- Individual assignee-specific tasks
|
||||
- Tasks fetched via other methods
|
||||
- Other report types
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Current Data Flow
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Current Implementation"
|
||||
A[fetch_all_tasks_with_due_dates] --> B[_make_search_request]
|
||||
B --> C[JIRA API]
|
||||
C --> D[Task Data]
|
||||
D --> E[format_task_data]
|
||||
E --> F[Report Generation]
|
||||
F --> G[Spreadsheet Export]
|
||||
end
|
||||
|
||||
subgraph "Current Task Data Structure"
|
||||
T1[issue]
|
||||
T2[start]
|
||||
T3[resolved]
|
||||
T4[due_date]
|
||||
T5[labels]
|
||||
T6[responsible]
|
||||
end
|
||||
```
|
||||
|
||||
### Current Task Data Structure
|
||||
The current task data includes:
|
||||
- issue: Task key
|
||||
- start: Creation date
|
||||
- resolved: Resolution date
|
||||
- due_date: Due date
|
||||
- labels: Task labels
|
||||
- responsible: Responsible person
|
||||
|
||||
## Target Architecture
|
||||
|
||||
### Enhanced Data Flow
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Enhanced Implementation"
|
||||
A[fetch_all_tasks_with_due_dates] --> B[_make_search_request]
|
||||
B --> C[JIRA API]
|
||||
C --> D[Task Data]
|
||||
D --> E[Process Parent Stories]
|
||||
E --> F{Parent Story Cache}
|
||||
F -->|Cache Hit| G[Use Cached Data]
|
||||
F -->|Cache Miss| H[Fetch Parent Story]
|
||||
H --> I[Update Cache]
|
||||
I --> J[Merge Task + Story Data]
|
||||
G --> J
|
||||
J --> K[format_task_data]
|
||||
K --> L[Report Generation]
|
||||
L --> M[Spreadsheet Export]
|
||||
end
|
||||
|
||||
subgraph "Enhanced Task Data Structure"
|
||||
T1[issue]
|
||||
T2[start]
|
||||
T3[resolved]
|
||||
T4[due_date]
|
||||
T5[labels]
|
||||
T6[responsible]
|
||||
T7[parent_story]
|
||||
T8[storypoints]
|
||||
end
|
||||
```
|
||||
|
||||
### Parent Story Link Structure
|
||||
Based on the example JSON (`WPCMU-9082.json`), the parent story information is located in:
|
||||
- Path: `fields.issuelinks`
|
||||
- Filter: `type.inward = "Parent task"`
|
||||
- Parent Key: `inwardIssue.key`
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
|
||||
#### 1.1 Create Parent Story Cache Manager
|
||||
**File**: `src/api/story_cache.py`
|
||||
|
||||
```python
|
||||
class StoryCache:
|
||||
"""
|
||||
Manages caching of parent story data to optimize API calls
|
||||
when multiple tasks belong to the same parent story.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {} # {story_key: storypoints}
|
||||
|
||||
def get(self, story_key: str) -> Optional[int]:
|
||||
"""Get storypoints from cache"""
|
||||
|
||||
def set(self, story_key: str, storypoints: int) -> None:
|
||||
"""Set storypoints in cache"""
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the cache"""
|
||||
```
|
||||
|
||||
#### 1.2 Create Parent Story Fetcher
|
||||
**File**: `src/api/client_jira.py`
|
||||
|
||||
```python
|
||||
def _fetch_parent_story_data(jira_url: str, bearer_token: str, story_key: str) -> Optional[Dict]:
|
||||
"""
|
||||
Fetches parent story data including storypoints
|
||||
|
||||
Args:
|
||||
jira_url: URL Jira API
|
||||
bearer_token: Bearer токен для аутентификации
|
||||
story_key: Key of the parent story
|
||||
|
||||
Returns:
|
||||
Dictionary with story data or None if error
|
||||
"""
|
||||
# Implementation to fetch single story by key
|
||||
# Extract storypoints from customfield_10003
|
||||
# Return cached data structure
|
||||
```
|
||||
|
||||
### Phase 2: Integration with Task Fetching
|
||||
|
||||
#### 2.1 Modify fetch_all_tasks_with_due_dates
|
||||
**File**: `src/api/client_jira.py`
|
||||
|
||||
```python
|
||||
def fetch_all_tasks_with_due_dates(
|
||||
jira_url: str,
|
||||
bearer_token: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
max_results: int = 50,
|
||||
story_cache: Optional[StoryCache] = None
|
||||
) -> Optional[List[Dict]]:
|
||||
"""
|
||||
Enhanced version that includes parent story and storypoints data
|
||||
"""
|
||||
# Initialize cache if not provided
|
||||
# Fetch tasks as before
|
||||
# For each task:
|
||||
# - Extract parent story key from issuelinks
|
||||
# - Check cache for storypoints
|
||||
# - Fetch from API if not in cache
|
||||
# - Update cache
|
||||
# - Add parent_story and storypoints to task data
|
||||
```
|
||||
|
||||
#### 2.2 Update Task Data Structure
|
||||
**File**: `src/formatting/reports_data_formatter.py`
|
||||
|
||||
```python
|
||||
def format_task_data(task_issue: Dict, target_tz: Optional[ZoneInfo]) -> Dict:
|
||||
"""
|
||||
Enhanced version that includes parent story and storypoints
|
||||
"""
|
||||
# Extract parent story information
|
||||
parent_story = None
|
||||
storypoints = None
|
||||
|
||||
# Extract from issuelinks where type.inward = "Parent task"
|
||||
issuelinks = task_issue.get('fields', {}).get('issuelinks', [])
|
||||
for link in issuelinks:
|
||||
if link.get('type', {}).get('inward') == 'Parent task':
|
||||
parent_story = link.get('inwardIssue', {}).get('key')
|
||||
break
|
||||
|
||||
# Get storypoints from the enhanced task data
|
||||
storypoints = task_issue.get('parent_storypoints')
|
||||
|
||||
return {
|
||||
"issue": issue_key,
|
||||
"start": formatted_created_date,
|
||||
"resolved": formatted_resolution_date,
|
||||
"due_date": formatted_due_date,
|
||||
"labels": labels_str,
|
||||
"responsible": formatted_responsible,
|
||||
"parent_story": parent_story,
|
||||
"storypoints": storypoints
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Report Integration
|
||||
|
||||
#### 3.1 Update GlobalDataManager with Cache Pre-population
|
||||
**File**: `src/reports/report_generator.py`
|
||||
|
||||
```python
|
||||
class GlobalDataManager:
|
||||
def __init__(self, jira_url: str, bearer_token: str):
|
||||
self.jira_url = jira_url
|
||||
self.bearer_token = bearer_token
|
||||
self.cached_bugs = None
|
||||
self.cached_stories = None
|
||||
self.cached_tasks = None
|
||||
self.story_cache = StoryCache() # New cache manager
|
||||
|
||||
def _populate_story_cache(self, stories: List[Dict]) -> None:
|
||||
"""
|
||||
Pre-populates the story cache with all stories and their storypoints
|
||||
to optimize task processing later.
|
||||
|
||||
Args:
|
||||
stories: List of story dictionaries from JIRA API
|
||||
"""
|
||||
for story in stories:
|
||||
story_key = story.get('key')
|
||||
storypoints = story.get('fields', {}).get('customfield_10003')
|
||||
if story_key and storypoints is not None:
|
||||
self.story_cache.set(story_key, storypoints)
|
||||
|
||||
def fetch_global_data(self, start_date: str, end_date: str, max_results: int = 50) -> Dict:
|
||||
results = {}
|
||||
|
||||
# Get stories first and populate cache
|
||||
self.cached_stories = fetch_all_stories_with_due_dates(
|
||||
self.jira_url, self.bearer_token, start_date, end_date, max_results
|
||||
)
|
||||
results['stories'] = self.cached_stories or []
|
||||
|
||||
# Pre-populate cache with story data for optimization
|
||||
if self.cached_stories:
|
||||
self._populate_story_cache(self.cached_stories)
|
||||
|
||||
# Get bugs
|
||||
self.cached_bugs = fetch_all_bugs(
|
||||
self.jira_url, self.bearer_token, start_date, end_date, max_results
|
||||
)
|
||||
results['bugs'] = self.cached_bugs or []
|
||||
|
||||
# Get tasks with pre-populated cache
|
||||
self.cached_tasks = fetch_all_tasks_with_due_dates(
|
||||
self.jira_url, self.bearer_token, start_date, end_date, max_results, self.story_cache
|
||||
)
|
||||
results['tasks'] = self.cached_tasks or []
|
||||
|
||||
return results
|
||||
```
|
||||
|
||||
#### 3.2 Update Report Transformation
|
||||
**File**: `src/reports/transform_report_data.py`
|
||||
|
||||
```python
|
||||
def _transform_task_section(report_data):
|
||||
"""
|
||||
Enhanced version that includes storypoints
|
||||
"""
|
||||
transformed_data = []
|
||||
for task in report_data:
|
||||
transformed_data.append([
|
||||
task['issue'],
|
||||
task['start'],
|
||||
task['resolved'],
|
||||
task['due_date'],
|
||||
task['labels'],
|
||||
task['responsible'],
|
||||
task['parent_story'], # New field
|
||||
task['storypoints'] # New field
|
||||
])
|
||||
return transformed_data
|
||||
```
|
||||
|
||||
#### 3.3 Update Spreadsheet Formatter
|
||||
**File**: `src/formatting/spreadsheet_formatter.py`
|
||||
|
||||
```python
|
||||
def format_task_sheet(reports_data):
|
||||
"""
|
||||
Formats data for 'Tasks' sheet as a single long vertical list.
|
||||
Returns: List of lists for sheet data
|
||||
"""
|
||||
headers = ["Task", "Start", "Resolved", "Due Date", "Labels", "Responsible", "Parent Story", "Story Points"]
|
||||
return _format_single_list_sheet_global_data(reports_data['global_tasks'], headers)
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Parent Story Extraction Logic
|
||||
|
||||
```python
|
||||
def _extract_parent_story_key(task_data: Dict) -> Optional[str]:
|
||||
"""
|
||||
Extracts parent story key from task issuelinks
|
||||
|
||||
Args:
|
||||
task_data: Task data from JIRA API
|
||||
|
||||
Returns:
|
||||
Parent story key or None if not found
|
||||
"""
|
||||
issuelinks = task_data.get('fields', {}).get('issuelinks', [])
|
||||
|
||||
for link in issuelinks:
|
||||
link_type = link.get('type', {})
|
||||
if link_type.get('inward') == 'Parent task':
|
||||
inward_issue = link.get('inwardIssue')
|
||||
if inward_issue:
|
||||
return inward_issue.get('key')
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
Simple in-memory cache without expiration for maximum performance:
|
||||
|
||||
```python
|
||||
def _get_or_fetch_storypoints(
|
||||
story_key: str,
|
||||
jira_url: str,
|
||||
bearer_token: str,
|
||||
story_cache: StoryCache
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Gets storypoints from cache or fetches from API
|
||||
|
||||
Args:
|
||||
story_key: Parent story key
|
||||
jira_url: URL Jira API
|
||||
bearer_token: Bearer токен для аутентификации
|
||||
story_cache: Story cache instance
|
||||
|
||||
Returns:
|
||||
Storypoints value or None if not found
|
||||
"""
|
||||
# Check cache first
|
||||
cached_points = story_cache.get(story_key)
|
||||
if cached_points is not None:
|
||||
return cached_points
|
||||
|
||||
# Fetch from API
|
||||
story_data = _fetch_parent_story_data(jira_url, bearer_token, story_key)
|
||||
if story_data:
|
||||
storypoints = story_data.get('fields', {}).get('customfield_10003')
|
||||
story_cache.set(story_key, storypoints)
|
||||
return storypoints
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
**Cache Design:**
|
||||
- Simple dictionary-based storage: `{story_key: storypoints}`
|
||||
- No expiration policy (cache lasts for the duration of report generation)
|
||||
- Automatic clearing when GlobalDataManager is reset
|
||||
- Minimal memory footprint as only storypoints are stored
|
||||
|
||||
## Data Flow Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant ReportGen as Report Generator
|
||||
participant GlobalDM as GlobalDataManager
|
||||
participant JiraAPI as JIRA API
|
||||
participant StoryCache as Story Cache
|
||||
|
||||
Client->>ReportGen: Generate Report
|
||||
ReportGen->>GlobalDM: fetch_global_data
|
||||
|
||||
Note over GlobalDM: Phase 1: Fetch Stories and Pre-populate Cache
|
||||
GlobalDM->>JiraAPI: fetch_all_stories_with_due_dates
|
||||
JiraAPI-->>GlobalDM: Stories List
|
||||
GlobalDM->>StoryCache: Pre-populate with all story storypoints
|
||||
|
||||
Note over GlobalDM: Phase 2: Fetch Tasks with Optimized Cache
|
||||
GlobalDM->>JiraAPI: fetch_all_tasks_with_due_dates
|
||||
JiraAPI-->>GlobalDM: Task List
|
||||
|
||||
loop For Each Task
|
||||
GlobalDM->>GlobalDM: Extract Parent Story Key
|
||||
alt Parent Story Found
|
||||
GlobalDM->>StoryCache: get(story_key)
|
||||
alt Cache Hit (Optimized)
|
||||
StoryCache-->>GlobalDM: Cached Storypoints
|
||||
else Cache Miss (Rare)
|
||||
GlobalDM->>JiraAPI: _fetch_parent_story_data
|
||||
JiraAPI-->>GlobalDM: Story Data
|
||||
GlobalDM->>StoryCache: set(story_key, storypoints)
|
||||
end
|
||||
GlobalDM->>GlobalDM: Merge Task + Story Data
|
||||
end
|
||||
end
|
||||
|
||||
GlobalDM-->>ReportGen: Enhanced Task Data
|
||||
ReportGen-->>Client: Report with Storypoints
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### API Error Handling
|
||||
- Handle cases where parent story is not accessible
|
||||
- Handle cases where story doesn't exist
|
||||
- Implement retry logic for failed API calls
|
||||
- Log errors appropriately
|
||||
|
||||
### Cache Error Handling
|
||||
- Handle cache initialization failures
|
||||
- Implement cache size limits
|
||||
- Handle cache corruption scenarios
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching Benefits
|
||||
- Reduces API calls when multiple tasks share the same parent story
|
||||
- Improves response time for large task sets
|
||||
- Reduces load on JIRA API
|
||||
- **Cache pre-population optimization**: Stories are fetched before tasks, so most parent story data is already cached when processing tasks
|
||||
|
||||
### Cache Pre-population Strategy
|
||||
The implementation includes a key optimization: the cache is pre-populated with all stories and their storypoints before processing tasks. This provides significant benefits:
|
||||
|
||||
1. **Higher cache hit rate**: Since stories are fetched first, most parent stories referenced by tasks will already be in cache
|
||||
2. **Reduced API calls**: Minimizes additional API requests during task processing
|
||||
3. **Faster processing**: Tasks can be processed more efficiently with cached data
|
||||
|
||||
### Memory Management
|
||||
- Simple in-memory cache without expiration
|
||||
- Cache cleared when GlobalDataManager is reset
|
||||
- Minimal memory footprint as only storypoints are stored
|
||||
- Cache is populated once per report generation cycle
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create StoryCache class in `src/api/story_cache.py`
|
||||
2. Create parent story fetcher function in `src/api/client_jira.py`
|
||||
3. Modify `fetch_all_tasks_with_due_dates` to include parent story data
|
||||
4. Update `format_task_data` to include parent story and storypoints
|
||||
5. Update `_transform_task_section` to include new fields
|
||||
6. Update `format_task_sheet` to include new columns
|
||||
7. Update GlobalDataManager to use story cache with pre-population
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
The service runs in Docker Compose and can be tested by sending requests to the running service.
|
||||
|
||||
### Test Procedure
|
||||
1. Start the service with `docker compose up`
|
||||
2. Send a test request to generate a report:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8055/create_report_in_spreadsheet" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"table_id": "1HRhWYzVql1cTv0mfGPd1B0qMXA5yD0xchirlfQJJjEo",
|
||||
"secret": "BRgln-IUjja-1kH6B-XONN9"
|
||||
}'
|
||||
```
|
||||
3. If the report generates successfully without errors, the implementation is working correctly
|
||||
4. The actual validation of the table data and storypoints will be performed manually
|
||||
@@ -0,0 +1,7 @@
|
||||
В данном проекте, в методе, который запирает все таски глобально fetch_all_tasks_with_due_dates() в client_jira.py необходимо добавить новый функционал. К каждой таске необходимо ещё получать дополнительную информацию – родительскую стори и сторипоинты родительской стори. Брать родительскую стори данной таски, из данной стори брать стори-поинты (stripoints), которые там есть, и добавлять потом к таске. При этом важно, поскольку тасок много, и некоторые из них принадлежат одной стори, то необходимо сделать кэширование. То есть мы берём, загружаем таску, лезем проверять её родительскую стори, забираем эти данные и кэшируем стори, чтобы в следующей, когда мы берём следующую таску, скорее всего, она попадёт в эту же стори. Тогда мы сначала проверяем кэш. В кеше достаточно хранить только словарь где ключи стори, а значение – сторипоинты. Если в кэше нет, тогда уже делаем запрос в жиру на получение родиельской стори и её сторипоинтов. То есть это нужно будет делать для каждой таски. Спроектируй архитектуру данных изменений и сохрани план работ в папке docs. Там же можно найти примеры планов от другого проекта. Необходимо аккуратно спроектировать данные изменения и аккуратно имплементировать во все места, где это может использоваться по флоу проекта. А ну и да, нужно будет в итоге в отчете добавить новое поле storypoints, по аналогии с отчетом, где собираются стори.
|
||||
|
||||
Информация о родительской таске находится в `fields.issuelinks`. Там список линков для данной таски. Нам нужна та у которой `type.inward == "Parent task"`. Там уже нужно найти "key" и это будет ключ на родительскую задачу. Забрать информацию по рдительской задаче и там найти сторипоинты. В проекте уже реализован сбор сторипоинтов. Если не указано, то поле пустое
|
||||
|
||||
Пример полной выдачи информации по таски от жиры в `docs\WPCMU-9082.json`
|
||||
|
||||
ВАЖНО! Всегда думай и отвечай на английском!
|
||||
Reference in New Issue
Block a user