Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F8391482
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Subscribers
None
View Options
diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js
index 07c3d2f9..3d23afb0 100644
--- a/assets/src/bundles/add_forge/create-request.js
+++ b/assets/src/bundles/add_forge/create-request.js
@@ -1,128 +1,155 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import {handleFetchError, removeUrlFragment, csrfPost,
getHumanReadableDate} from 'utils/functions';
+import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs';
+import {swhSpinnerSrc} from 'utils/constants';
let requestBrowseTable;
+const addForgeCheckboxId = 'swh-add-forge-user-filter';
+const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({
+ 'inputId': addForgeCheckboxId,
+ 'checked': true // by default, display only user requests
+});
+
export function onCreateRequestPageLoad() {
$(document).ready(() => {
$('#requestCreateForm').submit(async function(event) {
event.preventDefault();
try {
const response = await csrfPost($(this).attr('action'),
{'Content-Type': 'application/x-www-form-urlencoded'},
$(this).serialize());
handleFetchError(response);
$('#userMessageDetail').empty();
$('#userMessage').text('Your request has been submitted');
$('#userMessage').removeClass('badge-danger');
$('#userMessage').addClass('badge-success');
requestBrowseTable.draw(); // redraw the table to update the list
} catch (errorResponse) {
$('#userMessageDetail').empty();
let errorMessage;
let errorMessageDetail = '';
const errorData = await errorResponse.json();
// if (errorResponse.content_type === 'text/plain') { // does not work?
if (errorResponse.status === 409) {
errorMessage = errorData;
} else { // assuming json response
// const exception = errorData['exception'];
errorMessage = 'An unknown error occurred during the request creation';
try {
const reason = JSON.parse(errorData['reason']);
Object.entries(reason).forEach((keys, _) => {
const key = keys[0];
const message = keys[1][0]; // take only the first issue
errorMessageDetail += `\n${key}: ${message}`;
});
} catch (_) {
errorMessageDetail = errorData['reason']; // can't parse it, leave it raw
}
}
$('#userMessage').text(
errorMessageDetail ? `Error: ${errorMessageDetail}` : errorMessage
);
$('#userMessage').removeClass('badge-success');
$('#userMessage').addClass('badge-danger');
}
});
$('#swh-add-forge-requests-list-tab').on('shown.bs.tab', () => {
+ requestBrowseTable.draw();
window.location.hash = '#browse-requests';
});
$('#swh-add-forge-requests-help-tab').on('shown.bs.tab', () => {
window.location.hash = '#help';
});
$('#swh-add-forge-tab').on('shown.bs.tab', () => {
removeUrlFragment();
});
$(window).on('hashchange', () => {
onPageHashChage();
});
onPageHashChage(); // Explicit call to handle a hash during the page load
populateRequestBrowseList(); // Load existing requests
});
}
function onPageHashChage() {
if (window.location.hash === '#browse-requests') {
$('.nav-tabs a[href="#swh-add-forge-requests-list"]').tab('show');
} else if (window.location.hash === '#help') {
$('.nav-tabs a[href="#swh-add-forge-requests-help"]').tab('show');
} else {
$('.nav-tabs a[href="#swh-add-forge-submit-request"]').tab('show');
}
}
export function populateRequestBrowseList() {
requestBrowseTable = $('#add-forge-request-browse')
.on('error.dt', (e, settings, techNote, message) => {
$('#add-forge-browse-request-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
+ language: {
+ processing: `<img src="${swhSpinnerSrc}"></img>`
+ },
retrieve: true,
searching: true,
info: false,
- dom: '<<"d-flex justify-content-between align-items-center"f' +
- '<"#list-exclude">l>rt<"bottom"ip>>',
+ // Layout configuration, see [1] for more details
+ // [1] https://datatables.net/reference/option/dom
+ dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
+ '<"row"<"col-sm-12"tr>>' +
+ '<"row"<"col-sm-5"i><"col-sm-7"p>>',
ajax: {
- 'url': Urls.add_forge_request_list_datatables()
+ 'url': Urls.add_forge_request_list_datatables(),
+ data: (d) => {
+ if (swh.webapp.isUserLoggedIn() && $(`#${addForgeCheckboxId}`).prop('checked')) {
+ d.user_requests_only = '1';
+ }
+ }
+ },
+ fnInitComplete: function() {
+ if (swh.webapp.isUserLoggedIn()) {
+ $('div.user-requests-filter').html(userRequestsFilterCheckbox);
+ $(`#${addForgeCheckboxId}`).on('change', () => {
+ requestBrowseTable.draw();
+ });
+ }
},
columns: [
{
data: 'submission_date',
name: 'submission_date',
render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type'
},
{
data: 'forge_url',
name: 'forge_url'
},
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
- requestBrowseTable.draw();
}
diff --git a/cypress/integration/add-forge-now-request-create.spec.js b/cypress/integration/add-forge-now-request-create.spec.js
index 4b533060..d9612f4e 100644
--- a/cypress/integration/add-forge-now-request-create.spec.js
+++ b/cypress/integration/add-forge-now-request-create.spec.js
@@ -1,183 +1,255 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
function populateForm(type, url, contact, email, consent, comment) {
cy.get('#swh-input-forge-type').select(type);
- cy.get('#swh-input-forge-url').type(url);
- cy.get('#swh-input-forge-contact-name').type(contact);
- cy.get('#swh-input-forge-contact-email').type(email);
- cy.get('#swh-input-forge-comment').type(comment);
+ cy.get('#swh-input-forge-url').clear().type(url, {delay: 0, force: true});
+ cy.get('#swh-input-forge-contact-name').clear().type(contact, {delay: 0, force: true});
+ cy.get('#swh-input-forge-contact-email').clear().type(email, {delay: 0, force: true});
+ if (comment) {
+ cy.get('#swh-input-forge-comment').clear().type(comment, {delay: 0, force: true});
+ }
cy.get('#swh-input-consent-check').click({force: consent === 'on'});
}
+describe('Browse requests list tests', function() {
+ beforeEach(function() {
+ this.addForgeNowUrl = this.Urls.forge_add();
+ this.listAddForgeRequestsUrl = this.Urls.add_forge_request_list_datatables();
+ });
+
+ it('should not show user requests filter checkbox for anonymous users', function() {
+ cy.visit(this.addForgeNowUrl);
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('not.exist');
+ });
+
+ it('should show user requests filter checkbox for authenticated users', function() {
+ cy.userLogin();
+ cy.visit(this.addForgeNowUrl);
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+ });
+
+ it('should only display user requests when filter is activated', function() {
+ // Clean up previous state
+ cy.task('db:add_forge_now:delete');
+ // 'user2' logs in and create requests
+ cy.user2Login();
+ cy.visit(this.addForgeNowUrl);
+
+ // create requests for the user 'user'
+ populateForm('gitlab', 'gitlab.org', 'admin', 'admin@example.org', 'on', '');
+ cy.get('#requestCreateForm').submit();
+
+ // user requests filter checkbox should be in the DOM
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+
+ // check unfiltered user requests
+ cy.get('tbody tr').then(rows => {
+ expect(rows.length).to.eq(1);
+ });
+
+ // user1 logout
+ cy.contains('a', 'logout').click();
+
+ // user logs in
+ cy.userLogin();
+ cy.visit(this.addForgeNowUrl);
+
+ populateForm('gitea', 'gitea.org', 'admin', 'admin@example.org', 'on', '');
+ cy.get('#requestCreateForm').submit();
+ populateForm('cgit', 'cgit.org', 'admin', 'admin@example.org', 'on', '');
+ cy.get('#requestCreateForm').submit();
+
+ // user requests filter checkbox should be in the DOM
+ cy.get('#swh-add-forge-requests-list-tab').click();
+ cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+
+ // check unfiltered user requests
+ cy.get('tbody tr').then(rows => {
+ expect(rows.length).to.eq(2);
+ });
+
+ cy.get('#swh-add-forge-user-filter')
+ .uncheck({force: true});
+
+ // Users now sees everything
+ cy.get('tbody tr').then(rows => {
+ expect(rows.length).to.eq(2 + 1);
+ });
+ });
+});
+
describe('Test add-forge-request creation', function() {
beforeEach(function() {
this.addForgeNowUrl = this.Urls.forge_add();
});
it('should show all the tabs for every user', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-tab')
.should('have.class', 'nav-link');
cy.get('#swh-add-forge-requests-list-tab')
.should('have.class', 'nav-link');
cy.get('#swh-add-forge-requests-help-tab')
.should('have.class', 'nav-link');
});
it('should show create forge tab by default', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
});
it('should show login link for anonymous user', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#loginLink')
.should('be.visible')
.should('contain', 'log in');
});
it('should bring back after login', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#loginLink')
.should('have.attr', 'href')
.and('include', `${this.Urls.login()}?next=${this.Urls.forge_add()}`);
});
it('should change tabs on click', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('not.have.class', 'active');
cy.hash().should('eq', '#browse-requests');
cy.get('#swh-add-forge-requests-help-tab').click();
cy.get('#swh-add-forge-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('have.class', 'active');
cy.hash().should('eq', '#help');
cy.get('#swh-add-forge-tab').click();
cy.get('#swh-add-forge-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('not.have.class', 'active');
cy.hash().should('eq', '');
});
it('should show create form elements to authenticated user', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-input-forge-type')
.should('be.visible');
cy.get('#swh-input-forge-url')
.should('be.visible');
cy.get('#swh-input-forge-contact-name')
.should('be.visible');
cy.get('#swh-input-consent-check')
.should('be.visible');
cy.get('#swh-input-forge-comment')
.should('be.visible');
cy.get('#swh-input-form-submit')
.should('be.visible');
});
it('should show browse requests table for every user', function() {
// testing only for anonymous
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#add-forge-request-browse')
.should('be.visible');
cy.get('#loginLink')
.should('not.be.visible');
});
it('should update browse list on successful submission', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment');
cy.get('#requestCreateForm').submit();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#add-forge-request-browse')
.should('be.visible')
.should('contain', 'gitlab.com');
cy.get('#add-forge-request-browse')
.should('be.visible')
.should('contain', 'Pending');
});
it('should show error message on conflict', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment');
cy.get('#requestCreateForm').submit();
cy.get('#requestCreateForm').submit(); // Submitting the same data again
cy.get('#userMessage')
.should('have.class', 'badge-danger')
.should('contain', 'already exists');
});
it('should show error message', function() {
cy.userLogin();
cy.intercept('POST', `${this.Urls.api_1_add_forge_request_create()}**`,
{
body: {
'exception': 'BadInputExc',
'reason': '{"add-forge-comment": ["This field is required"]}'
},
statusCode: 400
}).as('errorRequest');
cy.visit(this.addForgeNowUrl);
populateForm(
'bitbucket', 'gitlab.com', 'test', 'test@example.com', 'off', 'comment'
);
cy.get('#requestCreateForm').submit();
cy.wait('@errorRequest').then((xhr) => {
cy.get('#userMessage')
.should('have.class', 'badge-danger')
.should('contain', 'field is required');
});
});
});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 01245fcc..eeb7373b 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -1,152 +1,160 @@
/**
* Copyright (C) 2019-2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
const axios = require('axios');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
async function httpGet(url) {
const response = await axios.get(url);
return response.data;
}
async function getMetadataForOrigin(originUrl, baseUrl) {
const originVisitsApiUrl = `${baseUrl}/api/1/origin/${originUrl}/visits`;
const originVisits = await httpGet(originVisitsApiUrl);
const lastVisit = originVisits[0];
const snapshotApiUrl = `${baseUrl}/api/1/snapshot/${lastVisit.snapshot}`;
const lastOriginSnapshot = await httpGet(snapshotApiUrl);
let revision = lastOriginSnapshot.branches.HEAD.target;
if (lastOriginSnapshot.branches.HEAD.target_type === 'alias') {
revision = lastOriginSnapshot.branches[revision].target;
}
const revisionApiUrl = `${baseUrl}/api/1/revision/${revision}`;
const lastOriginHeadRevision = await httpGet(revisionApiUrl);
return {
'directory': lastOriginHeadRevision.directory,
'revision': lastOriginHeadRevision.id,
'snapshot': lastOriginSnapshot.id
};
};
function getDatabase() {
return new sqlite3.Database('./swh-web-test.sqlite3');
}
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config);
// produce JSON files prior launching browser in order to dynamically generate tests
on('before:browser:launch', function(browser, launchOptions) {
return new Promise((resolve) => {
const p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`);
const p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`);
Promise.all([p1, p2])
.then(function(responses) {
fs.writeFileSync('cypress/fixtures/source-file-extensions.json', JSON.stringify(responses[0].data));
fs.writeFileSync('cypress/fixtures/source-file-names.json', JSON.stringify(responses[1].data));
resolve();
});
});
});
on('task', {
getSwhTestsData: async() => {
if (!global.swhTestsData) {
const swhTestsData = {};
swhTestsData.unarchivedRepo = {
url: 'https://github.com/SoftwareHeritage/swh-web',
type: 'git',
revision: '7bf1b2f489f16253527807baead7957ca9e8adde',
snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d',
rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663',
content: [{
sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859'
}]
};
swhTestsData.origin = [{
url: 'https://github.com/memononen/libtess2',
type: 'git',
content: [{
path: 'Source/tess.h'
}, {
path: 'premake4.lua'
}],
directory: [{
path: 'Source',
id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd'
}],
revisions: [],
invalidSubDir: 'Source1'
}, {
url: 'https://github.com/wcoder/highlightjs-line-numbers.js',
type: 'git',
content: [{
path: 'src/highlightjs-line-numbers.js'
}],
directory: [],
revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'],
release: {
name: 'v2.6.0',
id: '6877028d6e5412780517d0bfa81f07f6c51abb41',
directory: '5b61d50ef35ca9a4618a3572bde947b8cccf71ad'
}
}];
for (const origin of swhTestsData.origin) {
const metadata = await getMetadataForOrigin(origin.url, config.baseUrl);
const directoryApiUrl = `${config.baseUrl}/api/1/directory/${metadata.directory}`;
origin.dirContent = await httpGet(directoryApiUrl);
origin.rootDirectory = metadata.directory;
origin.revisions.push(metadata.revision);
origin.snapshot = metadata.snapshot;
for (const content of origin.content) {
const contentPathApiUrl = `${config.baseUrl}/api/1/directory/${origin.rootDirectory}/${content.path}`;
const contentMetaData = await httpGet(contentPathApiUrl);
content.name = contentMetaData.name.split('/').slice(-1)[0];
content.sha1git = contentMetaData.target;
content.directory = contentMetaData.dir_id;
const rawFileUrl = `${config.baseUrl}/browse/content/sha1_git:${content.sha1git}/raw/?filename=${content.name}`;
const fileText = await httpGet(rawFileUrl);
const fileLines = fileText.split('\n');
content.numberLines = fileLines.length;
if (!fileLines[content.numberLines - 1]) {
// If last line is empty its not shown
content.numberLines -= 1;
}
}
}
global.swhTestsData = swhTestsData;
}
return global.swhTestsData;
},
'db:user_mailmap:delete': () => {
const db = getDatabase();
db.serialize(function() {
db.run('DELETE FROM user_mailmap');
db.run('DELETE FROM user_mailmap_event');
});
db.close();
return true;
},
'db:user_mailmap:mark_processed': () => {
const db = getDatabase();
db.serialize(function() {
db.run('UPDATE user_mailmap SET mailmap_last_processing_date=datetime("now", "+1 hour")');
});
db.close();
return true;
+ },
+ 'db:add_forge_now:delete': () => {
+ const db = getDatabase();
+ db.serialize(function() {
+ db.run('DELETE FROM add_forge_now_request');
+ });
+ db.close();
+ return true;
}
});
return config;
};
diff --git a/cypress/support/index.js b/cypress/support/index.js
index 1c09614d..e703ec17 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -1,106 +1,110 @@
/**
* Copyright (C) 2019-2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import 'cypress-hmr-restarter';
import '@cypress/code-coverage/support';
Cypress.Screenshot.defaults({
screenshotOnRunFailure: false
});
Cypress.Commands.add('xhrShouldBeCalled', (alias, timesCalled) => {
const testRoutes = cy.state('routes');
const aliasRoute = Cypress._.find(testRoutes, {alias});
expect(Object.keys(aliasRoute.requests || {})).to.have.length(timesCalled);
});
function loginUser(username, password) {
const url = '/admin/login/';
return cy.request({
url: url,
method: 'GET'
}).then(() => {
cy.getCookie('sessionid').should('not.exist');
cy.getCookie('csrftoken').its('value').then((token) => {
cy.request({
url: url,
method: 'POST',
form: true,
followRedirect: false,
body: {
username: username,
password: password,
csrfmiddlewaretoken: token
}
}).then(() => {
cy.getCookie('sessionid').should('exist');
return cy.getCookie('csrftoken').its('value');
});
});
});
}
Cypress.Commands.add('adminLogin', () => {
return loginUser('admin', 'admin');
});
Cypress.Commands.add('userLogin', () => {
return loginUser('user', 'user');
});
+Cypress.Commands.add('user2Login', () => {
+ return loginUser('user2', 'user2');
+});
+
Cypress.Commands.add('ambassadorLogin', () => {
return loginUser('ambassador', 'ambassador');
});
Cypress.Commands.add('depositLogin', () => {
return loginUser('deposit', 'deposit');
});
Cypress.Commands.add('addForgeModeratorLogin', () => {
return loginUser('add-forge-moderator', 'add-forge-moderator');
});
function mockCostlyRequests() {
cy.intercept('https://status.softwareheritage.org/**', {
body: {
'result': {
'status': [
{
'id': '5f7c4c567f50b304c1e7bd5f',
'name': 'Save Code Now',
'updated': '2020-11-30T13:51:21.151Z',
'status': 'Operational',
'status_code': 100
}
]
}
}}).as('swhPlatformStatus');
cy.intercept('/coverage', {
body: ''
}).as('swhCoverageWidget');
}
Cypress.Commands.add('mailmapAdminLogin', () => {
return loginUser('mailmap-admin', 'mailmap-admin');
});
before(function() {
mockCostlyRequests();
cy.task('getSwhTestsData').then(testsData => {
Object.assign(this, testsData);
});
cy.visit('/').window().then(async win => {
this.Urls = win.Urls;
});
});
beforeEach(function() {
mockCostlyRequests();
});
diff --git a/swh/web/tests/create_test_users.py b/swh/web/tests/create_test_users.py
index 968de489..2ffc8902 100644
--- a/swh/web/tests/create_test_users.py
+++ b/swh/web/tests/create_test_users.py
@@ -1,50 +1,51 @@
# Copyright (C) 2021-2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from typing import Dict, List, Tuple
from django.contrib.auth import get_user_model
from swh.web.auth.utils import (
ADD_FORGE_MODERATOR_PERMISSION,
ADMIN_LIST_DEPOSIT_PERMISSION,
- SWH_AMBASSADOR_PERMISSION,
MAILMAP_ADMIN_PERMISSION,
+ SWH_AMBASSADOR_PERMISSION,
)
from swh.web.tests.utils import create_django_permission
User = get_user_model()
users: Dict[str, Tuple[str, str, List[str]]] = {
"user": ("user", "user@example.org", []),
+ "user2": ("user2", "user2@example.org", []),
"ambassador": (
"ambassador",
"ambassador@example.org",
[SWH_AMBASSADOR_PERMISSION],
),
"deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION]),
"add-forge-moderator": (
"add-forge-moderator",
"moderator@example.org",
[ADD_FORGE_MODERATOR_PERMISSION],
),
"mailmap-admin": (
"mailmap-admin",
"mailmap-admin@example.org",
[MAILMAP_ADMIN_PERMISSION],
),
}
for username, (password, email, permissions) in users.items():
if not User.objects.filter(username=username).exists():
user = User.objects.create_user(username, email, password)
if permissions:
for perm_name in permissions:
permission = create_django_permission(perm_name)
user.user_permissions.add(permission)
user.save()
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jun 4 2025, 6:44 PM (14 w, 15 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3272204
Attached To
rDWAPPS Web applications
Event Timeline
Log In to Comment