Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dc90a96501
commit
535401c636
|
@ -3,6 +3,7 @@ extends:
|
|||
- plugin:@gitlab/i18n
|
||||
- plugin:no-jquery/slim
|
||||
- plugin:no-jquery/deprecated-3.4
|
||||
- plugin:no-unsanitized/DOM
|
||||
- ./tooling/eslint-config/conditionally_ignore.js
|
||||
globals:
|
||||
__webpack_public_path__: true
|
||||
|
@ -116,6 +117,14 @@ rules:
|
|||
vue/multi-word-component-names: off
|
||||
unicorn/prefer-dom-node-dataset:
|
||||
- error
|
||||
no-unsanitized/method:
|
||||
- error
|
||||
- escape:
|
||||
methods: 'sanitize'
|
||||
no-unsanitized/property:
|
||||
- error
|
||||
- escape:
|
||||
methods: 'sanitize'
|
||||
overrides:
|
||||
- files:
|
||||
- '{,ee/,jh/}spec/frontend*/**/*'
|
||||
|
@ -134,6 +143,8 @@ overrides:
|
|||
message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
|
||||
- selector: ImportSpecifier[imported.name='GlSkeletonLoading']
|
||||
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
|
||||
no-unsanitized/method: off
|
||||
no-unsanitized/property: off
|
||||
- files:
|
||||
- 'config/**/*'
|
||||
- 'scripts/**/*'
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -482,7 +482,7 @@ gem 'net-ntp'
|
|||
gem 'ssh_data', '~> 1.3'
|
||||
|
||||
# Spamcheck GRPC protocol definitions
|
||||
gem 'spamcheck', '~> 0.1.0'
|
||||
gem 'spamcheck', '~> 1.0.0'
|
||||
|
||||
# Gitaly GRPC protocol definitions
|
||||
gem 'gitaly', '~> 15.3.0-rc4'
|
||||
|
|
|
@ -1314,7 +1314,7 @@ GEM
|
|||
sorted_set (1.0.3)
|
||||
rbtree
|
||||
set (~> 1.0)
|
||||
spamcheck (0.1.0)
|
||||
spamcheck (1.0.0)
|
||||
grpc (~> 1.0)
|
||||
spring (2.1.1)
|
||||
spring-commands-rspec (1.0.4)
|
||||
|
@ -1746,7 +1746,7 @@ DEPENDENCIES
|
|||
slack-messenger (~> 2.3.4)
|
||||
snowplow-tracker (~> 0.6.1)
|
||||
solargraph (~> 0.45.0)
|
||||
spamcheck (~> 0.1.0)
|
||||
spamcheck (~> 1.0.0)
|
||||
spring (~> 2.1.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
sprite-factory (~> 1.7)
|
||||
|
|
|
@ -165,6 +165,7 @@ export class AwardsHandler {
|
|||
`;
|
||||
|
||||
const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
|
||||
this.addRemainingEmojiMenuCategories();
|
||||
|
@ -198,6 +199,7 @@ export class AwardsHandler {
|
|||
emojisInCategory,
|
||||
);
|
||||
requestAnimationFrame(() => {
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
|
||||
resolve();
|
||||
});
|
||||
|
|
|
@ -67,6 +67,7 @@ export default {
|
|||
},
|
||||
content() {
|
||||
const el = document.createElement('div');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
el.innerHTML = this.draft.note_html;
|
||||
|
||||
return el.textContent;
|
||||
|
|
|
@ -22,6 +22,7 @@ class CopyCodeButton extends HTMLElement {
|
|||
'data-clipboard-target': `pre#${this.for}`,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
button.innerHTML = spriteIcon('copy-to-clipboard');
|
||||
|
||||
return button;
|
||||
|
|
|
@ -91,6 +91,7 @@ class SafeMathRenderer {
|
|||
`;
|
||||
|
||||
if (!wrapperElement.classList.contains('lazy-alert-shown')) {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
wrapperElement.innerHTML = html;
|
||||
wrapperElement.append(codeElement);
|
||||
wrapperElement.classList.add('lazy-alert-shown');
|
||||
|
@ -111,6 +112,7 @@ class SafeMathRenderer {
|
|||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
displayContainer.innerHTML = this.katex.renderToString(text, {
|
||||
displayMode: el.dataset.mathStyle === 'display',
|
||||
throwOnError: true,
|
||||
|
|
|
@ -45,6 +45,7 @@ const loadViewer = (viewerParam) => {
|
|||
viewer.dataset.loading = 'true';
|
||||
|
||||
return axios.get(url).then(({ data }) => {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
viewer.innerHTML = data.html;
|
||||
|
||||
window.requestIdleCallback(() => {
|
||||
|
|
|
@ -23,6 +23,7 @@ const wrapTextWithSpan = (el, text) => {
|
|||
|
||||
const wrapNodes = (text) => {
|
||||
const wrapper = createSpan();
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
wrapper.innerHTML = wrapSpacesWithSpans(text);
|
||||
wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text));
|
||||
return wrapper.childNodes;
|
||||
|
|
|
@ -13,6 +13,7 @@ const renderersByType = {
|
|||
},
|
||||
header(element, data) {
|
||||
element.classList.add('dropdown-header');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
element.innerHTML = data.content;
|
||||
|
||||
return element;
|
||||
|
@ -122,6 +123,7 @@ function assignTextToLink(el, data, options) {
|
|||
const text = getLinkText(data, options);
|
||||
|
||||
if (options.icon || options.highlight) {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
el.innerHTML = text;
|
||||
} else {
|
||||
el.textContent = text;
|
||||
|
|
|
@ -81,6 +81,7 @@ export default class FilterableList {
|
|||
|
||||
onFilterSuccess(response, queryData) {
|
||||
if (response.data.html) {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.listHolderElement.innerHTML = response.data.html;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
|
|||
const name = valueElement.innerText;
|
||||
const emojiTag = this.glEmojiTag(name);
|
||||
const emojiElement = dropdownItem.querySelector('gl-emoji');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
emojiElement.outerHTML = emojiTag;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -107,7 +107,7 @@ class DropDown {
|
|||
}
|
||||
|
||||
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
renderableList.innerHTML = children.join('');
|
||||
|
||||
const listEvent = new CustomEvent('render.dl', {
|
||||
|
@ -121,7 +121,7 @@ class DropDown {
|
|||
renderChildren(data) {
|
||||
const html = utils.template(this.templateString, data);
|
||||
const template = document.createElement('div');
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
template.innerHTML = html;
|
||||
DropDown.setImagesSrc(template);
|
||||
template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';
|
||||
|
|
|
@ -42,6 +42,7 @@ class HookButton extends Hook {
|
|||
}
|
||||
|
||||
restoreInitialState() {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.list.list.innerHTML = this.list.initialState;
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ class HookInput extends Hook {
|
|||
}
|
||||
|
||||
restoreInitialState() {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.list.list.innerHTML = this.list.initialState;
|
||||
}
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ export default class FilteredSearchVisualTokens {
|
|||
const hasOperator = Boolean(operator);
|
||||
|
||||
if (value) {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
|
||||
canEdit,
|
||||
uppercaseTokenName,
|
||||
|
@ -138,6 +139,7 @@ export default class FilteredSearchVisualTokens {
|
|||
operatorHTML = '<div class="operator"></div>';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
li.innerHTML = nameHTML + operatorHTML;
|
||||
}
|
||||
|
||||
|
@ -160,6 +162,8 @@ export default class FilteredSearchVisualTokens {
|
|||
if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
|
||||
const name = FilteredSearchVisualTokens.getLastTokenPartial();
|
||||
const operator = FilteredSearchVisualTokens.getLastTokenOperator();
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
|
||||
hasOperator: Boolean(operator),
|
||||
});
|
||||
|
@ -293,6 +297,7 @@ export default class FilteredSearchVisualTokens {
|
|||
const button = lastVisualToken.querySelector('.selectable');
|
||||
const valueContainer = lastVisualToken.querySelector('.value-container');
|
||||
button.removeChild(valueContainer);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
lastVisualToken.innerHTML = button.innerHTML;
|
||||
} else if (operator) {
|
||||
lastVisualToken.removeChild(operator);
|
||||
|
|
|
@ -47,6 +47,7 @@ export default class VisualTokenValue {
|
|||
|
||||
/* eslint-disable no-param-reassign */
|
||||
tokenValueContainer.dataset.originalValue = tokenValue;
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
tokenValueElement.innerHTML = `
|
||||
<img class="avatar s20" src="${user.avatar_url}" alt="">
|
||||
${escape(user.name)}
|
||||
|
@ -152,6 +153,7 @@ export default class VisualTokenValue {
|
|||
}
|
||||
|
||||
container.dataset.originalValue = value;
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
element.innerHTML = Emoji.glEmojiTag(value);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -236,11 +236,13 @@ const createFlash = function createFlash({
|
|||
|
||||
if (!flashContainer) return null;
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
flashContainer.innerHTML = createFlashEl(message, type);
|
||||
|
||||
const flashEl = flashContainer.querySelector(`.flash-${type}`);
|
||||
|
||||
if (actionConfig) {
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig));
|
||||
|
||||
if (actionConfig.clickHandler) {
|
||||
|
|
|
@ -30,6 +30,7 @@ export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
|
|||
|
||||
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
|
||||
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
buttonEl.innerHTML = spriteIcon('image-comment-dark');
|
||||
|
||||
containerEl.appendChild(buttonEl);
|
||||
|
|
|
@ -8,6 +8,7 @@ export function addCommentIndicator(containerEl, { x, y }) {
|
|||
buttonEl.style.left = `${x}px`;
|
||||
buttonEl.style.top = `${y}px`;
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
buttonEl.innerHTML = spriteIcon('image-comment-dark');
|
||||
|
||||
containerEl.appendChild(buttonEl);
|
||||
|
|
|
@ -226,6 +226,7 @@ export default {
|
|||
},
|
||||
createDragIconElement() {
|
||||
const container = document.createElement('div');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
|
||||
<use href="${gon.sprite_icons}#drag-vertical"></use>
|
||||
</svg>`;
|
||||
|
@ -358,6 +359,7 @@ export default {
|
|||
);
|
||||
button.id = `js-task-button-${index}`;
|
||||
this.taskButtons.push(button.id);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
button.innerHTML = `
|
||||
<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
|
||||
<use href="${gon.sprite_icons}#doc-new"></use>
|
||||
|
|
|
@ -13,6 +13,7 @@ const updateDescription = (descriptionHtml = '', details) => {
|
|||
}
|
||||
|
||||
const placeholder = document.createElement('div');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
placeholder.innerHTML = descriptionHtml;
|
||||
|
||||
const newDetails = placeholder.getElementsByTagName('details');
|
||||
|
|
|
@ -247,6 +247,7 @@ export default class LabelsSelect {
|
|||
}
|
||||
|
||||
linkEl.className = selectedClass.join(' ');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
linkEl.innerHTML = `${colorEl} ${escape(label.title)}`;
|
||||
|
||||
const listItemEl = document.createElement('li');
|
||||
|
|
|
@ -9,6 +9,7 @@ const setIframeRenderedSize = (h, w) => {
|
|||
const drawDiagram = (source) => {
|
||||
const element = document.getElementById('app');
|
||||
const insertSvg = (svgCode) => {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
element.innerHTML = svgCode;
|
||||
|
||||
const height = parseInt(element.firstElementChild.getAttribute('height'), 10);
|
||||
|
|
|
@ -119,6 +119,7 @@ const getAverageCharWidth = memoize(function getAverageCharWidth(options = {}) {
|
|||
div.style.left = -1000;
|
||||
div.style.top = -1000;
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
div.innerHTML = chars;
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
|
|
@ -434,6 +434,7 @@ export default class MergeRequestTabs {
|
|||
.get(`${source}.json`)
|
||||
.then(({ data }) => {
|
||||
const commitsDiv = document.querySelector('div#commits');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
commitsDiv.innerHTML = data.html;
|
||||
localTimeAgo(commitsDiv.querySelectorAll('.js-timeago'));
|
||||
this.commitsLoaded = true;
|
||||
|
|
|
@ -63,6 +63,8 @@ export default class PayloadPreviewer {
|
|||
|
||||
insertPayload(data) {
|
||||
this.isInserted = true;
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.getContainer().innerHTML = data;
|
||||
this.showPayload();
|
||||
}
|
||||
|
|
|
@ -200,9 +200,11 @@ export default class Todos {
|
|||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter(
|
||||
data.count,
|
||||
);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter(
|
||||
data.done_count,
|
||||
);
|
||||
|
|
|
@ -29,6 +29,7 @@ const selectEmojiCallback = (emoji, emojiTag) => {
|
|||
statusEmojiField.value = emoji;
|
||||
toggleNoEmojiPlaceholder(false);
|
||||
removeStatusEmoji();
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
toggleEmojiMenuButton.innerHTML += emojiTag;
|
||||
};
|
||||
|
||||
|
@ -74,6 +75,7 @@ Emoji.initEmojiMap()
|
|||
|
||||
if (hasStatusMessage) {
|
||||
toggleNoEmojiPlaceholder(false);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
|
||||
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
|
||||
toggleNoEmojiPlaceholder(true);
|
||||
|
|
|
@ -8,7 +8,10 @@ const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSk
|
|||
if (skippable) {
|
||||
const button = `<br/><a class="btn gl-button btn-sm btn-confirm gl-mt-3" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
|
||||
const flashAlert = document.querySelector('.flash-alert');
|
||||
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
|
||||
if (flashAlert) {
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
flashAlert.insertAdjacentHTML('beforeend', button);
|
||||
}
|
||||
}
|
||||
|
||||
mount2faRegistration();
|
||||
|
|
|
@ -33,6 +33,7 @@ export default class UserOverviewBlock {
|
|||
const containerEl = document.querySelector(this.container);
|
||||
const contentList = containerEl.querySelector('.overview-content-list');
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
contentList.innerHTML += html;
|
||||
|
||||
const loadingEl = containerEl.querySelector('.loading');
|
||||
|
|
|
@ -26,6 +26,7 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) {
|
|||
if (reason) {
|
||||
const optionTitle = option.querySelector('.js-visibility-level-radio span');
|
||||
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
reason.innerHTML = sprintf(
|
||||
__(
|
||||
'This project cannot be %{visibilityLevel} because the visibility of %{openShowLink}%{name}%{closeShowLink} is %{visibility}. To make this project %{visibilityLevel}, you must first %{openEditLink}change the visibility%{closeEditLink} of the parent group.',
|
||||
|
|
|
@ -22,11 +22,14 @@ export default class Star {
|
|||
starSpan.classList.remove('starred');
|
||||
starSpan.textContent = s__('StarProject|Star');
|
||||
starIcon.remove();
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
|
||||
} else {
|
||||
starSpan.classList.add('starred');
|
||||
starSpan.textContent = __('Unstar');
|
||||
starIcon.remove();
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
|
||||
}
|
||||
})
|
||||
|
|
|
@ -49,6 +49,9 @@ export default {
|
|||
error,
|
||||
});
|
||||
},
|
||||
context: {
|
||||
isSingleRequest: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -19,6 +19,7 @@ export default class InputValidator {
|
|||
setValidationMessage() {
|
||||
if (this.invalidInput) {
|
||||
this.inputDomElement.setCustomValidity(this.errorMessage);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.inputErrorMessage.innerHTML = this.errorMessage;
|
||||
} else {
|
||||
this.resetValidationMessage();
|
||||
|
@ -28,6 +29,7 @@ export default class InputValidator {
|
|||
resetValidationMessage() {
|
||||
if (this.inputDomElement.validationMessage === this.errorMessage) {
|
||||
this.inputDomElement.setCustomValidity('');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.inputErrorMessage.innerHTML = this.inputDomElement.title;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -428,6 +428,7 @@ export default {
|
|||
.then((res) => {
|
||||
if (res.data) {
|
||||
const el = document.createElement('div');
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
el.innerHTML = res.data;
|
||||
document.body.appendChild(el);
|
||||
document.dispatchEvent(new CustomEvent('merged:UpdateActions'));
|
||||
|
|
|
@ -163,6 +163,7 @@ export default {
|
|||
// resets the container HTML (replaces it with the updated noteHTML)
|
||||
// calls `renderSuggestions` once the updated noteHTML is added to the DOM
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.$refs.container.innerHTML = this.noteHtml;
|
||||
this.isRendered = false;
|
||||
this.renderSuggestions();
|
||||
|
|
|
@ -3,6 +3,8 @@ import { HLJS_COMMENT_SELECTOR } from '../constants';
|
|||
const createWrapper = (content) => {
|
||||
const span = document.createElement('span');
|
||||
span.className = HLJS_COMMENT_SELECTOR;
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
span.innerHTML = content;
|
||||
return span.outerHTML;
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ export default {
|
|||
mounted() {
|
||||
const legacyEntry = document.querySelector(this.selector);
|
||||
if (legacyEntry.tagName === 'TEMPLATE') {
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
this.$el.innerHTML = legacyEntry.innerHTML;
|
||||
} else {
|
||||
this.source = legacyEntry.parentNode;
|
||||
|
|
|
@ -20,6 +20,7 @@ const reloadMessage = LIVE_RELOAD
|
|||
? 'You have live_reload enabled, the page will reload automatically when complete.'
|
||||
: 'You have live_reload disabled, the page will reload automatically in a few seconds.';
|
||||
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
div.innerHTML = `
|
||||
<!-- https://github.com/webpack/media/blob/master/logo/icon-square-big.svg -->
|
||||
<svg height="50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
a:not(.canary-badge) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
|
@ -61,10 +61,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.canary-badge {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.project-item-select {
|
||||
right: auto;
|
||||
left: 0;
|
||||
|
|
|
@ -332,9 +332,6 @@ kbd kbd {
|
|||
color: #fff;
|
||||
background-color: #c17d10;
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.rounded-circle {
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
@ -815,20 +812,17 @@ kbd {
|
|||
.navbar-gitlab .header-content .title img {
|
||||
height: 24px;
|
||||
}
|
||||
.navbar-gitlab .header-content .title a {
|
||||
.navbar-gitlab .header-content .title a:not(.canary-badge) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 2px 4px -8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.navbar-gitlab .header-content .title a:active {
|
||||
.navbar-gitlab .header-content .title a:not(.canary-badge):active {
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px #1068bf;
|
||||
outline: none;
|
||||
}
|
||||
.navbar-gitlab .header-content .title .canary-badge {
|
||||
margin-left: -8px;
|
||||
}
|
||||
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
|
|
@ -311,9 +311,6 @@ kbd kbd {
|
|||
color: #fff;
|
||||
background-color: #ab6100;
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.rounded-circle {
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
@ -794,20 +791,17 @@ kbd {
|
|||
.navbar-gitlab .header-content .title img {
|
||||
height: 24px;
|
||||
}
|
||||
.navbar-gitlab .header-content .title a {
|
||||
.navbar-gitlab .header-content .title a:not(.canary-badge) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 2px 4px -8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.navbar-gitlab .header-content .title a:active {
|
||||
.navbar-gitlab .header-content .title a:not(.canary-badge):active {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 3px #63a6e9;
|
||||
outline: none;
|
||||
}
|
||||
.navbar-gitlab .header-content .title .canary-badge {
|
||||
margin-left: -8px;
|
||||
}
|
||||
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
|
|||
return unless current_jira_installation.instance_url?
|
||||
|
||||
request.content_security_policy.directives['connect-src'] ||= []
|
||||
request.content_security_policy.directives['connect-src'] << Gitlab::Utils.append_path(current_jira_installation.instance_url, '/-/jira_connect/oauth_application_ids')
|
||||
request.content_security_policy.directives['connect-src'] << Gitlab::Utils.append_path(current_jira_installation.instance_url, '/-/jira_connect/oauth_application_id')
|
||||
end
|
||||
|
||||
def create_service
|
||||
|
|
|
@ -91,7 +91,7 @@ class Snippet < ApplicationRecord
|
|||
participant :notes_with_associations
|
||||
|
||||
attr_spammable :title, spam_title: true
|
||||
attr_spammable :content, spam_description: true
|
||||
attr_spammable :description, spam_description: true
|
||||
|
||||
attr_encrypted :secret_token,
|
||||
key: Settings.attr_encrypted_db_key_base_truncated,
|
||||
|
@ -276,13 +276,7 @@ class Snippet < ApplicationRecord
|
|||
|
||||
def check_for_spam?(user:)
|
||||
visibility_level_changed?(to: Snippet::PUBLIC) ||
|
||||
(public? && (title_changed? || content_changed?))
|
||||
end
|
||||
|
||||
# snippets are the biggest sources of spam
|
||||
override :allow_possible_spam?
|
||||
def allow_possible_spam?
|
||||
false
|
||||
(public? && (title_changed? || description_changed?))
|
||||
end
|
||||
|
||||
def spammable_entity_type
|
||||
|
|
|
@ -73,6 +73,15 @@ module Snippets
|
|||
message
|
||||
end
|
||||
|
||||
def file_paths_to_commit
|
||||
paths = []
|
||||
snippet_actions.to_commit_actions.each do |action|
|
||||
paths << { path: action[:file_path] }
|
||||
end
|
||||
|
||||
paths
|
||||
end
|
||||
|
||||
def files_to_commit(snippet)
|
||||
snippet_actions.to_commit_actions.presence || build_actions_from_params(snippet)
|
||||
end
|
||||
|
|
|
@ -24,7 +24,8 @@ module Snippets
|
|||
spammable: @snippet,
|
||||
spam_params: spam_params,
|
||||
user: current_user,
|
||||
action: :create
|
||||
action: :create,
|
||||
extra_features: { files: file_paths_to_commit }
|
||||
).execute
|
||||
|
||||
if save_and_commit
|
||||
|
|
|
@ -23,11 +23,14 @@ module Snippets
|
|||
|
||||
update_snippet_attributes(snippet)
|
||||
|
||||
files = snippet.all_files.map { |f| { path: f } } + file_paths_to_commit
|
||||
|
||||
Spam::SpamActionService.new(
|
||||
spammable: snippet,
|
||||
spam_params: spam_params,
|
||||
user: current_user,
|
||||
action: :update
|
||||
action: :update,
|
||||
extra_features: { files: files }
|
||||
).execute
|
||||
|
||||
if save_and_commit(snippet)
|
||||
|
|
|
@ -4,11 +4,12 @@ module Spam
|
|||
class SpamActionService
|
||||
include SpamConstants
|
||||
|
||||
def initialize(spammable:, spam_params:, user:, action:)
|
||||
def initialize(spammable:, spam_params:, user:, action:, extra_features: {})
|
||||
@target = spammable
|
||||
@spam_params = spam_params
|
||||
@user = user
|
||||
@action = action
|
||||
@extra_features = extra_features
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
|
@ -40,7 +41,7 @@ module Spam
|
|||
|
||||
private
|
||||
|
||||
attr_reader :user, :action, :target, :spam_params, :spam_log
|
||||
attr_reader :user, :action, :target, :spam_params, :spam_log, :extra_features
|
||||
|
||||
##
|
||||
# In order to be proceed to the spam check process, the target must be
|
||||
|
@ -124,7 +125,9 @@ module Spam
|
|||
SpamVerdictService.new(target: target,
|
||||
user: user,
|
||||
options: options,
|
||||
context: context)
|
||||
context: context,
|
||||
extra_features: extra_features
|
||||
)
|
||||
end
|
||||
|
||||
def noteable_type
|
||||
|
|
|
@ -5,11 +5,12 @@ module Spam
|
|||
include AkismetMethods
|
||||
include SpamConstants
|
||||
|
||||
def initialize(user:, target:, options:, context: {})
|
||||
def initialize(user:, target:, options:, context: {}, extra_features: {})
|
||||
@target = target
|
||||
@user = user
|
||||
@options = options
|
||||
@context = context
|
||||
@extra_features = extra_features
|
||||
end
|
||||
|
||||
def execute
|
||||
|
@ -61,7 +62,7 @@ module Spam
|
|||
|
||||
private
|
||||
|
||||
attr_reader :user, :target, :options, :context
|
||||
attr_reader :user, :target, :options, :context, :extra_features
|
||||
|
||||
def akismet_verdict
|
||||
if akismet.spam?
|
||||
|
@ -75,7 +76,8 @@ module Spam
|
|||
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
|
||||
|
||||
begin
|
||||
result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
|
||||
result, attribs, _error = spamcheck_client.spam?(spammable: target, user: user, context: context,
|
||||
extra_features: extra_features)
|
||||
# @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
|
||||
|
||||
return [nil, attribs] unless result
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
%span.gl-sr-only GitLab
|
||||
= link_to root_path, title: _('Dashboard'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
|
||||
= brand_header_logo
|
||||
.gl-display-flex.gl-align-items-center
|
||||
- if Gitlab.com_and_canary?
|
||||
= link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do
|
||||
= gl_badge_tag({ variant: :success, size: :sm }) do
|
||||
= _('Next')
|
||||
= gl_badge_tag({ variant: :success, size: :sm }, { href: Gitlab::Saas.canary_toggle_com_url, data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer', class: 'canary-badge' }) do
|
||||
= _('Next')
|
||||
|
||||
- if current_user
|
||||
.gl-display-none.gl-sm-display-block
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
- name: "Manual iteration management" # The name of the feature to be deprecated
|
||||
announcement_milestone: "14.10" # The milestone when this feature was first announced as deprecated.
|
||||
announcement_date: "2022-04-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||
removal_milestone: "16.0" # The milestone when this feature is planned to be removed
|
||||
removal_date: "2023-04-22" # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||
breaking_change: true # If this deprecation is a breaking change, set this value to true
|
||||
reporter: mcelicalderonG # GitLab username of the person reporting the deprecation
|
||||
body: | # Do not modify this line, instead modify the lines below.
|
||||
Manual iteration management is deprecated and only automatic iteration cadences will be supported in the future.
|
||||
|
||||
Creating and deleting iterations will be fully removed in 16.0. Updating all iteration fields except for
|
||||
`description` will also be removed.
|
||||
|
||||
On the GraphQL API the following mutations will be removed:
|
||||
|
||||
1. `iterationCreate`
|
||||
1. `iterationDelete`
|
||||
|
||||
The update `updateIteration` mutation will only allow updating the iteration's `description`. The following
|
||||
arguments will be removed:
|
||||
|
||||
1. `title`
|
||||
1. `dueDate`
|
||||
1. `startDate`
|
||||
|
||||
For more information about iteration cadences, you can refer to
|
||||
[the documentation of the feature](https://docs.gitlab.com/ee/user/group/iterations/#iteration-cadences).
|
||||
# The following items are not published on the docs page, but may be used in the future.
|
||||
stage: Plan
|
||||
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356069
|
||||
documentation_url: # (optional) This is a link to the current documentation page
|
||||
image_url: # (optional) This is a link to a thumbnail image depicting the feature
|
||||
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
|
|
@ -60,7 +60,7 @@ class Gitlab::Seeder::Pipelines
|
|||
::Ci::ProcessPipelineService.new(pipeline).execute
|
||||
end
|
||||
|
||||
::Gitlab::Seeder::Ci::DailyBuildGroupReportResult.new(@project).seed if @project.last_pipeline
|
||||
::Gitlab::Seeders::Ci::DailyBuildGroupReportResult.new(@project).seed if @project.last_pipeline
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -361,38 +361,6 @@ In GitLab 15.0, for Dependency Scanning, the default version of Java that the sc
|
|||
|
||||
</div>
|
||||
|
||||
<div class="deprecation removal-160 breaking-change">
|
||||
|
||||
### Manual iteration management
|
||||
|
||||
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-04-22)
|
||||
|
||||
WARNING:
|
||||
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||
Review the details carefully before upgrading.
|
||||
|
||||
Manual iteration management is deprecated and only automatic iteration cadences will be supported in the future.
|
||||
|
||||
Creating and deleting iterations will be fully removed in 16.0. Updating all iteration fields except for
|
||||
`description` will also be removed.
|
||||
|
||||
On the GraphQL API the following mutations will be removed:
|
||||
|
||||
1. `iterationCreate`
|
||||
1. `iterationDelete`
|
||||
|
||||
The update `updateIteration` mutation will only allow updating the iteration's `description`. The following
|
||||
arguments will be removed:
|
||||
|
||||
1. `title`
|
||||
1. `dueDate`
|
||||
1. `startDate`
|
||||
|
||||
For more information about iteration cadences, you can refer to
|
||||
[the documentation of the feature](https://docs.gitlab.com/ee/user/group/iterations/#iteration-cadences).
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation removal-150 breaking-change">
|
||||
|
||||
### Outdated indices of Advanced Search migrations
|
||||
|
|
|
@ -12,11 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - Moved to GitLab Premium in 13.9.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) in GitLab 14.6. [Feature flag `group_iterations`](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) removed.
|
||||
|
||||
WARNING:
|
||||
After [Iteration Cadences](#iteration-cadences) becomes generally available,
|
||||
manual iteration scheduling will be [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 15.6.
|
||||
To enhance the role of iterations as time boundaries, we will also deprecate the title field.
|
||||
|
||||
Iterations are a way to track issues over a period of time. This allows teams
|
||||
to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
|
||||
for tracking over different time periods.
|
||||
|
@ -187,7 +182,7 @@ To group issues by label:
|
|||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1 [with a flag](../../../administration/feature_flags.md), named `iteration_cadences`. Disabled by default.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/354977) in GitLab 15.0: All scheduled iterations must start on the same day of the week as the cadence start day. Start date of cadence cannot be edited after the first iteration starts.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/354878) in GitLab 15.0.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/367493) in GitLab 15.3: A new automation start date can be selected for cadence. Upcoming iterations will be scheduled to start on the same day of the week as the changed start date.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/367493) in GitLab 15.4: A new automation start date can be selected for cadence. Upcoming iterations will be scheduled to start on the same day of the week as the changed start date. Iteration cadences can be manually managed by turning off the automatic scheduling feature.
|
||||
|
||||
Iteration cadences automate iteration scheduling. You can use them to
|
||||
automate creating iterations every 1, 2, 3, or 4 weeks. You can also
|
||||
|
@ -206,8 +201,9 @@ To create an iteration cadence:
|
|||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select **New iteration cadence**.
|
||||
1. Complete the fields.
|
||||
- Enter the title and description of the iteration cadence.
|
||||
1. Enter the title and description of the iteration cadence.
|
||||
1. To manually manage the iteration cadence, clear the **Enable automatic scheduling** checkbox and skip the next step.
|
||||
1. Complete the required fields to use automatic scheduling.
|
||||
- Select the automation start date of the iteration cadence. Iterations will be scheduled to
|
||||
begin on the same day of the week as the day of the week of the start date.
|
||||
- From the **Duration** dropdown list, select how many weeks each iteration should last.
|
||||
|
@ -228,7 +224,7 @@ To edit an iteration cadence:
|
|||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select **Edit iteration cadence**.
|
||||
|
||||
When you edit the **Automation start date** field,
|
||||
When you are using automatic scheduling and edit the **Automation start date** field,
|
||||
you must set a new start date that doesn't overlap with the existing
|
||||
current or past iterations.
|
||||
|
||||
|
@ -236,52 +232,23 @@ Editing **Upcoming iterations** is a non-destructive action.
|
|||
If ten upcoming iterations already exist, changing the number under **Upcoming iterations** to `2`
|
||||
doesn't delete the eight existing upcoming iterations.
|
||||
|
||||
### Delete an iteration cadence
|
||||
|
||||
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for a group.
|
||||
|
||||
Deleting an iteration cadence also deletes all iterations within that cadence.
|
||||
|
||||
To delete an iteration cadence:
|
||||
#### Turn on automatic scheduling for manual iterations cadence
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
|
||||
1. Select **Delete cadence** in the confirmation modal.
|
||||
|
||||
### Manual iteration cadences
|
||||
|
||||
When you **enable** the iteration cadences feature, all previously
|
||||
created iterations are added to a default iteration cadence.
|
||||
You can continue to add, edit, and remove iterations in
|
||||
this default cadence.
|
||||
|
||||
#### Convert a manual cadence to use automatic scheduling
|
||||
|
||||
WARNING:
|
||||
The upgrade is irreversible. After it's done, a new manual iteration cadence cannot be created.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have created [iterations](#iterations) without cadences before enabling iteration cadences for your group.
|
||||
To upgrade the iteration cadence to use the automation features:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence you want to upgrade.
|
||||
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence for which you want to enable automatic scheduling.
|
||||
1. Check the **Enable automatic scheduling** checkbox.
|
||||
1. Complete the required fields **Duration**, **Upcoming iterations**, and **Automation start date**.
|
||||
For **Automation start date**, you can select any date that doesn't overlap with the existing open iterations.
|
||||
If you have upcoming iterations, the automatic scheduling adjusts them appropriately to fit
|
||||
your chosen duration.
|
||||
1. Select **Save changes**.
|
||||
|
||||
#### Converted cadences example
|
||||
When you want to manage your iterations cadence manually again, edit your cadence and uncheck the **Enable automatic scheduling** checkbox.
|
||||
|
||||
For example, suppose it's Friday, April 15, and you have three iterations in a manual cadence:
|
||||
#### Example of turning on automatic scheduling for manual iterations cadence
|
||||
|
||||
Suppose it's Friday, April 15, and you have three iteration in a manual iterations cadence:
|
||||
|
||||
- Monday, April 4 - Friday, April 8 (closed)
|
||||
- Tuesday, April 12 - Friday, April 15 (ongoing)
|
||||
|
@ -305,3 +272,20 @@ is changed to "April 18 - Sunday, April 24".
|
|||
|
||||
An additional upcoming iteration "April 25 - Sunday, May 1" is scheduled
|
||||
to satisfy the requirement that there are at least two upcoming iterations scheduled.
|
||||
|
||||
### Delete an iteration cadence
|
||||
|
||||
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for a group.
|
||||
|
||||
Deleting an iteration cadence also deletes all iterations within that cadence.
|
||||
|
||||
To delete an iteration cadence:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
|
||||
1. Select **Delete cadence**.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
|
@ -13,15 +13,56 @@ When you have a lot of issues, it can be hard to get an overview.
|
|||
With weighted issues, you can get a better idea of how much time,
|
||||
value, or complexity a given issue has or costs.
|
||||
|
||||
You can set the weight of an issue during its creation, by changing the
|
||||
value in the dropdown menu. You can set it to a non-negative integer
|
||||
value from 0, 1, 2, and so on.
|
||||
You can remove weight from an issue as well.
|
||||
A user with a Reporter role (or above) can set the weight.
|
||||
## View the issue weight
|
||||
|
||||
This value appears on the right sidebar of an individual issue, as well as
|
||||
in the issues page next to a weight icon (**{weight}**).
|
||||
You can view the issue weight on:
|
||||
|
||||
As an added bonus, you can see the total sum of all issues on the milestone page.
|
||||
- The right sidebar of each issue.
|
||||
- The issues page, next to a weight icon (**{weight}**).
|
||||
- [Issue boards](../issue_board.md), next to a weight icon (**{weight}**).
|
||||
- The [milestone](../milestones/index.md) page, as a total sum of issue weights.
|
||||
|
||||
![issue page](img/issue_weight_v13_11.png)
|
||||
## Set the issue weight
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for the project.
|
||||
|
||||
You can set the issue weight when you create or edit an issue.
|
||||
|
||||
You must use whole numbers (like 0, 1, 2). Negative numbers or fractions are not accepted.
|
||||
|
||||
When you change the weight of an issue, the new value overwrites the previous value.
|
||||
|
||||
### When you create an issue
|
||||
|
||||
To set the issue weight when you [create an issue](managing_issues.md#create-an-issue), enter a
|
||||
number under **Weight**.
|
||||
|
||||
### From an existing issue
|
||||
|
||||
To set the issue weight from an existing issue:
|
||||
|
||||
1. Go to the issue.
|
||||
1. On the right sidebar, in the **Weight** section, select **Edit**.
|
||||
1. Enter the new weight.
|
||||
1. Select any area outside the dropdown list.
|
||||
|
||||
### From an issue board
|
||||
|
||||
To set the issue weight when you [edit an issue from an issue board](../issue_board.md#edit-an-issue):
|
||||
|
||||
1. Go to your issue board.
|
||||
1. Select an issue card (not its title).
|
||||
1. On the right sidebar, in the **Weight** section, select **Edit**.
|
||||
1. Enter the new weight.
|
||||
1. Select any area outside the dropdown list.
|
||||
|
||||
## Remove issue weight
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for the project.
|
||||
|
||||
To remove the issue weight, follow the same steps as when you [set the issue weight](#set-the-issue-weight),
|
||||
and select **remove weight**.
|
||||
|
|
|
@ -151,48 +151,6 @@ module Gitlab
|
|||
model.logger = old_loggers[connection_name]
|
||||
end
|
||||
end
|
||||
|
||||
module Ci
|
||||
class DailyBuildGroupReportResult
|
||||
DEFAULT_BRANCH = 'master'
|
||||
COUNT_OF_DAYS = 5
|
||||
|
||||
def initialize(project)
|
||||
@project = project
|
||||
@last_pipeline = project.last_pipeline
|
||||
end
|
||||
|
||||
def seed
|
||||
COUNT_OF_DAYS.times do |count|
|
||||
date = Time.now.utc - count.day
|
||||
create_report(date)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :last_pipeline
|
||||
|
||||
def create_report(date)
|
||||
last_pipeline.builds.uniq(&:group_name).each do |build|
|
||||
::Ci::DailyBuildGroupReportResult.create(
|
||||
project: project,
|
||||
last_pipeline: last_pipeline,
|
||||
date: date,
|
||||
ref_path: last_pipeline.source_ref_path,
|
||||
group_name: build.group_name,
|
||||
data: {
|
||||
'coverage' => rand(20..99)
|
||||
},
|
||||
group: project.group,
|
||||
default_branch: last_pipeline.default_branch?
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Seeders
|
||||
module Ci
|
||||
class DailyBuildGroupReportResult
|
||||
DEFAULT_BRANCH = 'master'
|
||||
COUNT_OF_DAYS = 5
|
||||
|
||||
def initialize(project)
|
||||
@project = project
|
||||
@last_pipeline = project.last_pipeline
|
||||
end
|
||||
|
||||
def seed
|
||||
COUNT_OF_DAYS.times do |count|
|
||||
date = Time.now.utc - count.day
|
||||
create_report(date)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :last_pipeline
|
||||
|
||||
def create_report(date)
|
||||
last_pipeline.builds.uniq(&:group_name).each do |build|
|
||||
::Ci::DailyBuildGroupReportResult.create(
|
||||
project: project,
|
||||
last_pipeline: last_pipeline,
|
||||
date: date,
|
||||
ref_path: last_pipeline.source_ref_path,
|
||||
group_name: build.group_name,
|
||||
data: {
|
||||
'coverage' => rand(20..99)
|
||||
},
|
||||
group: project.group,
|
||||
default_branch: last_pipeline.default_branch?
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -33,33 +33,50 @@ module Gitlab
|
|||
@endpoint_url = @endpoint_url.sub(URL_SCHEME_REGEX, '')
|
||||
end
|
||||
|
||||
def issue_spam?(spam_issue:, user:, context: {})
|
||||
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
|
||||
def spam?(spammable:, user:, context: {}, extra_features: {})
|
||||
metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key }
|
||||
protobuf_args = { spammable: spammable, user: user, context: context, extra_features: extra_features }
|
||||
|
||||
pb, grpc_method = build_protobuf(**protobuf_args)
|
||||
response = grpc_method.call(pb, metadata: metadata)
|
||||
|
||||
response = grpc_client.check_for_spam_issue(issue,
|
||||
metadata: { 'authorization' =>
|
||||
Gitlab::CurrentSettings.spam_check_api_key })
|
||||
verdict = convert_verdict_to_gitlab_constant(response.verdict)
|
||||
[verdict, response.extra_attributes.to_h, response.error]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_spammable_mappings(spammable)
|
||||
case spammable
|
||||
when Issue
|
||||
[::Spamcheck::Issue, grpc_client.method(:check_for_spam_issue)]
|
||||
when Snippet
|
||||
[::Spamcheck::Snippet, grpc_client.method(:check_for_spam_snippet)]
|
||||
else
|
||||
raise ArgumentError, "Not a spammable type: #{spammable.class.name}"
|
||||
end
|
||||
end
|
||||
|
||||
def convert_verdict_to_gitlab_constant(verdict)
|
||||
VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict)
|
||||
end
|
||||
|
||||
def build_issue_protobuf(issue:, user:, context:)
|
||||
issue_pb = ::Spamcheck::Issue.new
|
||||
issue_pb.title = issue.spam_title || ''
|
||||
issue_pb.description = issue.spam_description || ''
|
||||
issue_pb.created_at = convert_to_pb_timestamp(issue.created_at) if issue.created_at
|
||||
issue_pb.updated_at = convert_to_pb_timestamp(issue.updated_at) if issue.updated_at
|
||||
issue_pb.user_in_project = user.authorized_project?(issue.project)
|
||||
issue_pb.project = build_project_protobuf(issue)
|
||||
issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
|
||||
issue_pb.user = build_user_protobuf(user)
|
||||
issue_pb
|
||||
def build_protobuf(spammable:, user:, context:, extra_features:)
|
||||
protobuf_class, grpc_method = get_spammable_mappings(spammable)
|
||||
pb = protobuf_class.new(**extra_features)
|
||||
pb.title = spammable.spam_title || ''
|
||||
pb.description = spammable.spam_description || ''
|
||||
pb.created_at = convert_to_pb_timestamp(spammable.created_at) if spammable.created_at
|
||||
pb.updated_at = convert_to_pb_timestamp(spammable.updated_at) if spammable.updated_at
|
||||
pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
|
||||
pb.user = build_user_protobuf(user)
|
||||
|
||||
unless spammable.project.nil?
|
||||
pb.user_in_project = user.authorized_project?(spammable.project)
|
||||
pb.project = build_project_protobuf(spammable)
|
||||
end
|
||||
|
||||
[pb, grpc_method]
|
||||
end
|
||||
|
||||
def build_user_protobuf(user)
|
||||
|
|
|
@ -22084,9 +22084,6 @@ msgstr ""
|
|||
msgid "Iterations|Cadence name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Can be converted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Cancel"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22132,6 +22129,9 @@ msgstr ""
|
|||
msgid "Iterations|Edit iteration cadence"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Enable automatic scheduling"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Enable roll over"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22147,12 +22147,6 @@ msgstr ""
|
|||
msgid "Iterations|Iterations are scheduled to start on %{weekday}s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Learn more about automatic scheduling"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Manual management of iterations will be deprecated in GitLab 15.6. Convert your manual cadence to use automated scheduling when you are ready."
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Move incomplete issues to the next iteration."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22210,9 +22204,6 @@ msgstr ""
|
|||
msgid "Iterations|The iteration has been deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|This cadence can be converted to use automated scheduling"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|This will delete the cadence as well as all of the iterations within it."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22222,9 +22213,6 @@ msgstr ""
|
|||
msgid "Iterations|Title"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|To convert this cadence to automatic scheduling, add a duration and number of upcoming iterations. The upgrade is irreversible."
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Unable to find iteration cadence."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22237,9 +22225,6 @@ msgstr ""
|
|||
msgid "Iterations|Upcoming iterations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iterations|Your manual cadence can be converted to use automated scheduling"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -217,6 +217,7 @@
|
|||
"eslint-import-resolver-jest": "3.0.2",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-no-jquery": "2.7.0",
|
||||
"eslint-plugin-no-unsanitized": "^4.0.1",
|
||||
"gettext-extractor": "^3.5.3",
|
||||
"gettext-extractor-vue": "^5.0.0",
|
||||
"glob": "^7.1.6",
|
||||
|
|
|
@ -77,44 +77,4 @@ RSpec.describe Gitlab::Seeder do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ::Gitlab::Seeder::Ci::DailyBuildGroupReportResult do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :repository, group: group) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
|
||||
|
||||
subject(:build_report) do
|
||||
described_class.new(project)
|
||||
end
|
||||
|
||||
describe '#seed' do
|
||||
it 'creates daily build results for the project' do
|
||||
expect { build_report.seed }.to change {
|
||||
Ci::DailyBuildGroupReportResult.count
|
||||
}.by(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
|
||||
end
|
||||
|
||||
it 'matches project data with last report' do
|
||||
build_report.seed
|
||||
|
||||
report = project.daily_build_group_report_results.last
|
||||
reports_count = project.daily_build_group_report_results.count
|
||||
|
||||
expect(build.group_name).to eq(report.group_name)
|
||||
expect(pipeline.source_ref_path).to eq(report.ref_path)
|
||||
expect(pipeline.default_branch?).to eq(report.default_branch)
|
||||
expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
|
||||
end
|
||||
|
||||
it 'does not raise error on RecordNotUnique' do
|
||||
build_report.seed
|
||||
build_report.seed
|
||||
|
||||
reports_count = project.daily_build_group_report_results.count
|
||||
|
||||
expect(reports_count).to eq(Gitlab::Seeder::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::Seeders::Ci::DailyBuildGroupReportResult do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :repository, group: group) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
|
||||
|
||||
subject(:build_report) do
|
||||
described_class.new(project)
|
||||
end
|
||||
|
||||
describe '#seed' do
|
||||
it 'creates daily build results for the project' do
|
||||
expect { build_report.seed }.to change {
|
||||
Ci::DailyBuildGroupReportResult.count
|
||||
}.by(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
|
||||
end
|
||||
|
||||
it 'matches project data with last report' do
|
||||
build_report.seed
|
||||
|
||||
report = project.daily_build_group_report_results.last
|
||||
reports_count = project.daily_build_group_report_results.count
|
||||
|
||||
expect(build.group_name).to eq(report.group_name)
|
||||
expect(pipeline.source_ref_path).to eq(report.ref_path)
|
||||
expect(pipeline.default_branch?).to eq(report.default_branch)
|
||||
expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
|
||||
end
|
||||
|
||||
it 'does not raise error on RecordNotUnique' do
|
||||
build_report.seed
|
||||
build_report.seed
|
||||
|
||||
reports_count = project.daily_build_group_report_results.count
|
||||
|
||||
expect(reports_count).to eq(Gitlab::Seeders::Ci::DailyBuildGroupReportResult::COUNT_OF_DAYS)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,6 +17,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
end
|
||||
|
||||
let_it_be(:issue) { create(:issue, description: 'Test issue description') }
|
||||
let_it_be(:snippet) { create(:personal_snippet, :public, description: 'Test issue description') }
|
||||
|
||||
let(:response) do
|
||||
verdict = ::Spamcheck::SpamVerdict.new
|
||||
|
@ -26,7 +27,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
verdict
|
||||
end
|
||||
|
||||
subject { described_class.new.issue_spam?(spam_issue: issue, user: user) }
|
||||
subject { described_class.new.spam?(spammable: issue, user: user) }
|
||||
|
||||
before do
|
||||
stub_application_setting(spam_check_endpoint_url: endpoint)
|
||||
|
@ -56,10 +57,11 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#issue_spam?' do
|
||||
shared_examples 'check for spam' do
|
||||
before do
|
||||
allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance|
|
||||
allow(instance).to receive(:check_for_spam_issue).and_return(response)
|
||||
allow(instance).to receive(:check_for_spam_snippet).and_return(response)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -89,12 +91,26 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#build_issue_protobuf", :aggregate_failures do
|
||||
it 'builds the expected protobuf object' do
|
||||
describe "#spam?", :aggregate_failures do
|
||||
describe 'issue' do
|
||||
subject { described_class.new.spam?(spammable: issue, user: user) }
|
||||
|
||||
it_behaves_like "check for spam"
|
||||
end
|
||||
|
||||
describe 'snippet' do
|
||||
subject { described_class.new.spam?(spammable: snippet, user: user, extra_features: { files: [{ path: "file.rb" }] }) }
|
||||
|
||||
it_behaves_like "check for spam"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#build_protobuf", :aggregate_failures do
|
||||
it 'builds the expected issue protobuf object' do
|
||||
cxt = { action: :create }
|
||||
issue_pb = described_class.new.send(:build_issue_protobuf,
|
||||
issue: issue, user: user,
|
||||
context: cxt)
|
||||
issue_pb, _ = described_class.new.send(:build_protobuf,
|
||||
spammable: issue, user: user,
|
||||
context: cxt, extra_features: {})
|
||||
expect(issue_pb.title).to eq issue.title
|
||||
expect(issue_pb.description).to eq issue.description
|
||||
expect(issue_pb.user_in_project).to be false
|
||||
|
@ -104,6 +120,22 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
|
||||
expect(issue_pb.user.username).to eq user.username
|
||||
end
|
||||
|
||||
it 'builds the expected snippet protobuf object' do
|
||||
cxt = { action: :create }
|
||||
snippet_pb, _ = described_class.new.send(:build_protobuf,
|
||||
spammable: snippet, user: user,
|
||||
context: cxt, extra_features: { files: [{ path: 'first.rb' }, { path: 'second.rb' }] })
|
||||
expect(snippet_pb.title).to eq snippet.title
|
||||
expect(snippet_pb.description).to eq snippet.description
|
||||
expect(snippet_pb.created_at).to eq timestamp_to_protobuf_timestamp(snippet.created_at)
|
||||
expect(snippet_pb.updated_at).to eq timestamp_to_protobuf_timestamp(snippet.updated_at)
|
||||
expect(snippet_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
|
||||
expect(snippet_pb.user.username).to eq user.username
|
||||
expect(snippet_pb.user.username).to eq user.username
|
||||
expect(snippet_pb.files.first.path).to eq 'first.rb'
|
||||
expect(snippet_pb.files.last.path).to eq 'second.rb'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_user_protobuf', :aggregate_failures do
|
||||
|
@ -143,6 +175,19 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#get_spammable_mappings", :aggregate_failures do
|
||||
it 'is an expected spammable' do
|
||||
protobuf_class, _ = described_class.new.send(:get_spammable_mappings, issue)
|
||||
expect(protobuf_class).to eq ::Spamcheck::Issue
|
||||
end
|
||||
|
||||
it 'is an unexpected spammable' do
|
||||
expect { described_class.new.send(:get_spammable_mappings, 'spam') }.to raise_error(
|
||||
ArgumentError, 'Not a spammable type: String'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def timestamp_to_protobuf_timestamp(timestamp)
|
||||
|
|
|
@ -256,6 +256,7 @@ RSpec.describe API::ProjectSnippets do
|
|||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
stub_feature_flags(allow_possible_spam: false)
|
||||
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
@ -311,6 +312,8 @@ RSpec.describe API::ProjectSnippets do
|
|||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
|
||||
stub_feature_flags(allow_possible_spam: false)
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
|
|
|
@ -340,6 +340,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
|
|||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
stub_feature_flags(allow_possible_spam: false)
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
|
@ -405,6 +406,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
|
|||
allow_next_instance_of(Spam::AkismetService) do |instance|
|
||||
allow(instance).to receive(:spam?).and_return(true)
|
||||
end
|
||||
stub_feature_flags(allow_possible_spam: false)
|
||||
end
|
||||
|
||||
context 'when the snippet is private' do
|
||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
|||
|
||||
subject(:content_security_policy) { response.headers['Content-Security-Policy'] }
|
||||
|
||||
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_ids') }
|
||||
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/oauth_application_id') }
|
||||
|
||||
context 'with no self-managed instance configured' do
|
||||
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }
|
||||
|
|
|
@ -6,6 +6,8 @@ RSpec.describe Spam::SpamActionService do
|
|||
include_context 'includes Spam constants'
|
||||
|
||||
let(:issue) { create(:issue, project: project, author: author) }
|
||||
let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
|
||||
let(:project_snippet) { create(:project_snippet, :public, author: author) }
|
||||
let(:fake_ip) { '1.2.3.4' }
|
||||
let(:fake_user_agent) { 'fake-user-agent' }
|
||||
let(:fake_referer) { 'fake-http-referer' }
|
||||
|
@ -27,6 +29,7 @@ RSpec.describe Spam::SpamActionService do
|
|||
|
||||
before do
|
||||
issue.spam = false
|
||||
personal_snippet.spam = false
|
||||
end
|
||||
|
||||
describe 'constructor argument validation' do
|
||||
|
@ -50,24 +53,24 @@ RSpec.describe Spam::SpamActionService do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'creates a spam log' do
|
||||
shared_examples 'creates a spam log' do |target_type|
|
||||
it do
|
||||
expect { subject }
|
||||
.to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
|
||||
.to log_spam(title: target.title, description: target.description, noteable_type: target_type)
|
||||
|
||||
# TODO: These checks should be incorporated into the `log_spam` RSpec matcher above
|
||||
new_spam_log = SpamLog.last
|
||||
expect(new_spam_log.user_id).to eq(user.id)
|
||||
expect(new_spam_log.title).to eq(issue.title)
|
||||
expect(new_spam_log.description).to eq(issue.description)
|
||||
expect(new_spam_log.title).to eq(target.title)
|
||||
expect(new_spam_log.description).to eq(target.spam_description)
|
||||
expect(new_spam_log.source_ip).to eq(fake_ip)
|
||||
expect(new_spam_log.user_agent).to eq(fake_user_agent)
|
||||
expect(new_spam_log.noteable_type).to eq('Issue')
|
||||
expect(new_spam_log.noteable_type).to eq(target_type)
|
||||
expect(new_spam_log.via_api).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
shared_examples 'execute spam action service' do |target_type|
|
||||
let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
|
||||
let(:fake_verdict_service) { double(:spam_verdict_service) }
|
||||
let(:allowlisted) { false }
|
||||
|
@ -82,20 +85,22 @@ RSpec.describe Spam::SpamActionService do
|
|||
|
||||
let(:verdict_service_args) do
|
||||
{
|
||||
target: issue,
|
||||
target: target,
|
||||
user: user,
|
||||
options: verdict_service_opts,
|
||||
context: {
|
||||
action: :create,
|
||||
target_type: 'Issue'
|
||||
}
|
||||
target_type: target_type
|
||||
},
|
||||
extra_features: extra_features
|
||||
}
|
||||
end
|
||||
|
||||
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
|
||||
|
||||
subject do
|
||||
described_service = described_class.new(spammable: issue, spam_params: spam_params, user: user, action: :create)
|
||||
described_service = described_class.new(spammable: target, spam_params: spam_params, extra_features:
|
||||
extra_features, user: user, action: :create)
|
||||
allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
|
||||
described_service.execute
|
||||
end
|
||||
|
@ -136,7 +141,7 @@ RSpec.describe Spam::SpamActionService do
|
|||
|
||||
context 'when spammable attributes have not changed' do
|
||||
before do
|
||||
issue.closed_at = Time.zone.now
|
||||
allow(target).to receive(:has_changes_to_save?).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not create a spam log' do
|
||||
|
@ -146,11 +151,11 @@ RSpec.describe Spam::SpamActionService do
|
|||
|
||||
context 'when spammable attributes have changed' do
|
||||
let(:expected_service_check_response_message) do
|
||||
/Check Issue spammable model for any errors or CAPTCHA requirement/
|
||||
/Check #{target_type} spammable model for any errors or CAPTCHA requirement/
|
||||
end
|
||||
|
||||
before do
|
||||
issue.description = 'Lovely Spam! Wonderful Spam!'
|
||||
target.description = 'Lovely Spam! Wonderful Spam!'
|
||||
end
|
||||
|
||||
context 'when allowlisted' do
|
||||
|
@ -170,13 +175,13 @@ RSpec.describe Spam::SpamActionService do
|
|||
allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates a spam log'
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it 'marks as spam' do
|
||||
response = subject
|
||||
|
||||
expect(response.message).to match(expected_service_check_response_message)
|
||||
expect(issue).to be_spam
|
||||
expect(target).to be_spam
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -185,13 +190,13 @@ RSpec.describe Spam::SpamActionService do
|
|||
allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates a spam log'
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it 'marks as spam' do
|
||||
response = subject
|
||||
|
||||
expect(response.message).to match(expected_service_check_response_message)
|
||||
expect(issue).to be_spam
|
||||
expect(target).to be_spam
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -200,20 +205,20 @@ RSpec.describe Spam::SpamActionService do
|
|||
allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates a spam log'
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it 'does not mark as spam' do
|
||||
response = subject
|
||||
|
||||
expect(response.message).to match(expected_service_check_response_message)
|
||||
expect(issue).not_to be_spam
|
||||
expect(target).not_to be_spam
|
||||
end
|
||||
|
||||
it 'marks as needing reCAPTCHA' do
|
||||
response = subject
|
||||
|
||||
expect(response.message).to match(expected_service_check_response_message)
|
||||
expect(issue).to be_needs_recaptcha
|
||||
expect(target).to be_needs_recaptcha
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -222,20 +227,20 @@ RSpec.describe Spam::SpamActionService do
|
|||
allow(fake_verdict_service).to receive(:execute).and_return(OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates a spam log'
|
||||
it_behaves_like 'creates a spam log', target_type
|
||||
|
||||
it 'does not mark as spam' do
|
||||
response = subject
|
||||
|
||||
expect(response.message).to match(expected_service_check_response_message)
|
||||
expect(issue).not_to be_spam
|
||||
expect(target).not_to be_spam
|
||||
end
|
||||
|
||||
it 'does not mark as needing CAPTCHA' do
|
||||
response = subject
|
||||
|
||||
expect(response.message).to match(expected_service_check_response_message)
|
||||
expect(issue).not_to be_needs_recaptcha
|
||||
expect(target).not_to be_needs_recaptcha
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -249,7 +254,7 @@ RSpec.describe Spam::SpamActionService do
|
|||
end
|
||||
|
||||
it 'clears spam flags' do
|
||||
expect(issue).to receive(:clear_spam_flags!)
|
||||
expect(target).to receive(:clear_spam_flags!)
|
||||
|
||||
subject
|
||||
end
|
||||
|
@ -265,7 +270,7 @@ RSpec.describe Spam::SpamActionService do
|
|||
end
|
||||
|
||||
it 'clears spam flags' do
|
||||
expect(issue).to receive(:clear_spam_flags!)
|
||||
expect(target).to receive(:clear_spam_flags!)
|
||||
|
||||
subject
|
||||
end
|
||||
|
@ -285,4 +290,27 @@ RSpec.describe Spam::SpamActionService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
describe 'issue' do
|
||||
let(:target) { issue }
|
||||
let(:extra_features) { {} }
|
||||
|
||||
it_behaves_like 'execute spam action service', 'Issue'
|
||||
end
|
||||
|
||||
describe 'project snippet' do
|
||||
let(:target) { project_snippet }
|
||||
let(:extra_features) { { files: [{ path: 'project.rb' }] } }
|
||||
|
||||
it_behaves_like 'execute spam action service', 'ProjectSnippet'
|
||||
end
|
||||
|
||||
describe 'personal snippet' do
|
||||
let(:target) { personal_snippet }
|
||||
let(:extra_features) { { files: [{ path: 'personal.rb' }] } }
|
||||
|
||||
it_behaves_like 'execute spam action service', 'PersonalSnippet'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,9 +17,10 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:check_for_spam) { true }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:issue) { create(:issue, author: user) }
|
||||
let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
|
||||
|
||||
let(:service) do
|
||||
described_class.new(user: user, target: issue, options: {})
|
||||
described_class.new(user: user, target: target, options: {})
|
||||
end
|
||||
|
||||
let(:attribs) do
|
||||
|
@ -31,7 +32,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
stub_feature_flags(allow_possible_spam: false)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
shared_examples 'execute spam verdict service' do
|
||||
subject { service.execute }
|
||||
|
||||
before do
|
||||
|
@ -172,7 +173,8 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#akismet_verdict' do
|
||||
shared_examples 'akismet verdict' do
|
||||
let(:target) { issue }
|
||||
subject { service.send(:akismet_verdict) }
|
||||
|
||||
context 'if Akismet is enabled' do
|
||||
|
@ -227,7 +229,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#spamcheck_verdict' do
|
||||
shared_examples 'spamcheck verdict' do
|
||||
subject { service.send(:spamcheck_verdict) }
|
||||
|
||||
context 'if a Spam Check endpoint enabled and set to a URL' do
|
||||
|
@ -254,7 +256,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
|
||||
before do
|
||||
allow(service).to receive(:spamcheck_client).and_return(spam_client)
|
||||
allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error])
|
||||
allow(spam_client).to receive(:spam?).and_return([verdict, attribs, error])
|
||||
end
|
||||
|
||||
context 'if the result is a NOOP verdict' do
|
||||
|
@ -365,7 +367,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:attribs) { nil }
|
||||
|
||||
before do
|
||||
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted)
|
||||
allow(spam_client).to receive(:spam?).and_raise(GRPC::Aborted)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
|
@ -387,7 +389,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:attribs) { nil }
|
||||
|
||||
before do
|
||||
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded)
|
||||
allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
|
@ -416,4 +418,46 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
describe 'issue' do
|
||||
let(:target) { issue }
|
||||
|
||||
it_behaves_like 'execute spam verdict service'
|
||||
end
|
||||
|
||||
describe 'snippet' do
|
||||
let(:target) { snippet }
|
||||
|
||||
it_behaves_like 'execute spam verdict service'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#akismet_verdict' do
|
||||
describe 'issue' do
|
||||
let(:target) { issue }
|
||||
|
||||
it_behaves_like 'akismet verdict'
|
||||
end
|
||||
|
||||
describe 'snippet' do
|
||||
let(:target) { snippet }
|
||||
|
||||
it_behaves_like 'akismet verdict'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#spamcheck_verdict' do
|
||||
describe 'issue' do
|
||||
let(:target) { issue }
|
||||
|
||||
it_behaves_like 'spamcheck verdict'
|
||||
end
|
||||
|
||||
describe 'snippet' do
|
||||
let(:target) { snippet }
|
||||
|
||||
it_behaves_like 'spamcheck verdict'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,8 @@ RSpec.shared_examples 'checking spam' do
|
|||
spammable: kind_of(Snippet),
|
||||
spam_params: spam_params,
|
||||
user: an_instance_of(User),
|
||||
action: action
|
||||
action: action,
|
||||
extra_features: { files: an_instance_of(Array) }
|
||||
}
|
||||
) do |instance|
|
||||
expect(instance).to receive(:execute)
|
||||
|
|
|
@ -5192,6 +5192,11 @@ eslint-plugin-no-jquery@2.7.0:
|
|||
resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz#855f5631cf5b8e25b930cf6f06e02dd81f132e72"
|
||||
integrity sha512-Aeg7dA6GTH1AcWLlBtWNzOU9efK5KpNi7b0EhBO0o0M+awyzguUUo8gF6hXGjQ9n5h8/uRtYv9zOqQkeC5CG0w==
|
||||
|
||||
eslint-plugin-no-unsanitized@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.1.tgz#e2343265467ba2270ade478cbe07bbafeaea412d"
|
||||
integrity sha512-y/lAMWnPPC7RYuUdxlEL/XiCL8FehN9h9s3Kjqbp/Kv0i9NZs+IXSC2kS546Fa4Bumwy31HlVS/OdWX0Kxb5Xg==
|
||||
|
||||
eslint-plugin-promise@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
|
||||
|
|
Loading…
Reference in New Issue