Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Organizations] Include reserved namespaces and package ownership requests in Manage Packages page filtering #5491

Merged
merged 12 commits into from
Mar 1, 2018
Merged
16 changes: 11 additions & 5 deletions src/NuGetGallery/Controllers/PackagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,8 @@ private async Task<ActionResult> HandleOwnershipRequest(string id, string userna

if (package.Owners.Any(o => o.MatchesUser(user)))
{
// If the user is already an owner, clean up the invalid request.
await _packageOwnershipManagementService.DeletePackageOwnershipRequestAsync(package, user);
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably this should not throw.

return View("ConfirmOwner", new PackageOwnerConfirmationModel(id, user.Username, ConfirmOwnershipResult.AlreadyOwner));
}

Expand Down Expand Up @@ -1334,19 +1336,23 @@ private async Task<ActionResult> HandleOwnershipRequest(string id, string userna
[RequiresAccountConfirmation("cancel pending ownership request")]
public virtual async Task<ActionResult> CancelPendingOwnershipRequest(string id, string requestingUsername, string pendingUsername)
{
if (!string.Equals(requestingUsername, User.Identity.Name, StringComparison.OrdinalIgnoreCase))
var package = _packageService.FindPackageRegistrationById(id);
if (package == null)
{
return HttpNotFound();
}

if (ActionsRequiringPermissions.ManagePackageOwnership.CheckPermissionsOnBehalfOfAnyAccount(GetCurrentUser(), package) != PermissionsCheckResult.Allowed)
{
return View("ConfirmOwner", new PackageOwnerConfirmationModel(id, requestingUsername, ConfirmOwnershipResult.NotYourRequest));
}

var package = _packageService.FindPackageRegistrationById(id);
if (package == null)
var requestingUser = _userService.FindByUsername(requestingUsername);
if (requestingUser == null)
{
return HttpNotFound();
}

var requestingUser = GetCurrentUser();

var pendingUser = _userService.FindByUsername(pendingUsername);
if (pendingUser == null)
{
Expand Down
15 changes: 12 additions & 3 deletions src/NuGetGallery/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,11 +368,20 @@ public virtual ActionResult Packages()
.Select(p => new ListPackageItemViewModel(p, currentUser)).OrderBy(p => p.Id)
.ToList();

var received = _packageOwnerRequestService.GetPackageOwnershipRequests(newOwner: currentUser);
var sent = _packageOwnerRequestService.GetPackageOwnershipRequests(requestingOwner: currentUser);
var userReceived = _packageOwnerRequestService.GetPackageOwnershipRequests(newOwner: currentUser);
var orgReceived = currentUser.Organizations
.Where(m => ActionsRequiringPermissions.HandlePackageOwnershipRequest.CheckPermissions(currentUser, m.Organization) == PermissionsCheckResult.Allowed)
.SelectMany(m => _packageOwnerRequestService.GetPackageOwnershipRequests(newOwner: m.Organization));
var received = userReceived.Union(orgReceived);

var sent = packages.SelectMany(p => _packageOwnerRequestService.GetPackageOwnershipRequests(package: p.PackageRegistration));

var ownerRequests = new OwnerRequestsViewModel(received, sent, currentUser, _packageService);
var reservedPrefixes = new ReservedNamespaceListViewModel(currentUser.ReservedNamespaces);

var userReservedNamespaces = currentUser.ReservedNamespaces;
var organizationsReservedNamespaces = currentUser.Organizations.SelectMany(m => m.Organization.ReservedNamespaces);

var reservedPrefixes = new ReservedNamespaceListViewModel(userReservedNamespaces.Union(organizationsReservedNamespaces).ToArray());

var model = new ManagePackagesViewModel
{
Expand Down
2 changes: 0 additions & 2 deletions src/NuGetGallery/NuGetGallery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2029,10 +2029,8 @@
<Content Include="Views\Packages\_ImportReadMe.cshtml" />
<Content Include="Views\Packages\_SubmitPackage.cshtml" />
<Content Include="Views\Packages\_EditForm.cshtml" />
<Content Include="Views\Users\_OwnerRequestsList.cshtml" />
<Content Include="Areas\Admin\Views\DeleteAccount\DeleteUserAccount.cshtml" />
<Content Include="Views\Users\_UserPackagesListForDeletedAccount.cshtml" />
<Content Include="Views\Users\_ReservedNamespacesList.cshtml" />
<Content Include="Views\Users\DeleteAccount.cshtml" />
<Content Include="Views\Authentication\SignInNuGetAccount.cshtml" />
<Content Include="Views\Packages\_VerifyForm.cshtml" />
Expand Down
2 changes: 2 additions & 0 deletions src/NuGetGallery/RouteName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public static class RouteName
public const string UploadPackage = "UploadPackage";
public const string UploadPackageProgress = "UploadPackageProgress";
public const string PackageVersionAction = "PackageVersionAction";
public const string ConfirmPendingOwnershipRequest = "ConfirmPendingOwnershipRequest";
public const string PackageOwnerConfirmation = "PackageOwnerConfirmation";
public const string RejectPendingOwnershipRequest = "RejectPendingOwnershipRequest";
public const string PackageOwnerRejection = "PackageOwnerRejection";
public const string PackageOwnerCancellation = "PackageOwnerCancellation";
public const string PackageAction = "PackageAction";
Expand Down
147 changes: 144 additions & 3 deletions src/NuGetGallery/Scripts/gallery/page-manage-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

function showInitialPackagesData(dataSelector, packagesList) {
var downloadsCount = 0;
$.each(packagesList, function () { downloadsCount += this.TotalDownloadCount });
$.each(packagesList, function () { downloadsCount += this.TotalDownloadCount; });
$(dataSelector).text(formatPackagesData(packagesList.length, downloadsCount));
}

function formatPackagesData(packagesCount, downloadsCount) {
return packagesCount.toLocaleString()
+ ' package' + (packagesCount == 1 ? '' : 's')
+ ' package' + (packagesCount === 1 ? '' : 's')
+ ' / '
+ downloadsCount.toLocaleString()
+ ' download' + (downloadsCount == 1 ? '' : 's');
+ ' download' + (downloadsCount === 1 ? '' : 's');
Copy link
Member

Choose a reason for hiding this comment

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

awesome - thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was tired of those LINT errors haha

}

$(function () {
Expand Down Expand Up @@ -90,6 +90,141 @@
}, this);
}

function showInitialReservedNamespaceData(dataSelector, namespacesList) {
$(dataSelector).text(formatReservedNamespacesData(namespacesList.length));
}

function formatReservedNamespacesData(namespacesCount) {
return namespacesCount.toLocaleString() + " namespace" + (namespacesCount === 1 ? '' : 's');
}
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can add a pluralize helper in common.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a little skeptical about adding something like this given that
1 - the code here is (less than) a one-liner
2 - the actual problem of pluralization is very complex (if someone supplies "octopus" they might expect it to be pluralized to "octopi")

I'm fine with adding a function just for this file because it is used 3 times but beyond that scope might be misleading.


function ReservedNamespaceListItemViewModel(reservedNamespaceListViewModel, namespaceItem) {
var self = this;

this.ReservedNamespaceListViewModel = reservedNamespaceListViewModel;
this.Pattern = namespaceItem.Pattern;
this.SearchUrl = namespaceItem.SearchUrl;
this.Owners = namespaceItem.Owners;
this.IsPublic = namespaceItem.IsPublic;

this.Visible = ko.observable(true);

this.UpdateVisibility = function (ownerFilter) {
var visible = ownerFilter === "All packages";
if (!visible) {
for (var i in self.Owners) {
if (ownerFilter === self.Owners[i].Username) {
visible = true;
break;
}
}
}
this.Visible(visible);
};
}

function ReservedNamespaceListViewModel(managePackagesViewModel, namespaces) {
var self = this;

this.ManagePackagesViewModel = managePackagesViewModel;
this.Namespaces = $.map(namespaces, function (data) {
return new ReservedNamespaceListItemViewModel(self, data);
});
this.VisibleNamespacesCount = ko.observable();
this.VisibleNamespacesHeading = ko.pureComputed(function () {
return formatReservedNamespacesData(ko.unwrap(self.VisibleNamespacesCount()));
});

this.ManagePackagesViewModel.OwnerFilter.subscribe(function (newOwner) {
var namespacesCount = 0;
for (var i in self.Namespaces) {
self.Namespaces[i].UpdateVisibility(newOwner.Username);
if (self.Namespaces[i].Visible()) {
namespacesCount++;
}
}
this.VisibleNamespacesCount(namespacesCount);
}, this);
}

function showInitialOwnerRequestsData(dataSelector, requestsList) {
$(dataSelector).text(formatOwnerRequestsData(requestsList.length));
}

function formatOwnerRequestsData(requestsCount) {
return requestsCount.toLocaleString() + " request" + (requestsCount === 1 ? '' : 's');
}

function OwnerRequestsItemViewModel(ownerRequestsListViewModel, ownerRequestItem, showReceived, showSent) {
var self = this;

this.OwnerRequestsListViewModel = ownerRequestsListViewModel;
this.Id = ownerRequestItem.Id;
this.Requesting = ownerRequestItem.Requesting;
this.New = ownerRequestItem.New;
this.Owners = ownerRequestItem.Owners;
this.PackageIconUrl = ownerRequestItem.PackageIconUrl
? ownerRequestItem.PackageIconUrl
: this.OwnerRequestsListViewModel.ManagePackagesViewModel.DefaultPackageIconUrl;
this.PackageUrl = ownerRequestItem.PackageUrl;
this.CanAccept = ownerRequestItem.CanAccept;
this.CanCancel = ownerRequestItem.CanCancel;
this.ConfirmUrl = ownerRequestItem.ConfirmUrl;
this.RejectUrl = ownerRequestItem.RejectUrl;
this.CancelUrl = ownerRequestItem.CancelUrl;
this.ShowReceived = showReceived;
this.ShowSent = showSent;

this.Visible = ko.observable(true);

this.UpdateVisibility = function (ownerFilter) {
var visible = ownerFilter === "All packages";
if (!visible) {
if (self.ShowReceived && ownerFilter === self.New.Username) {
visible = true;
}

if (self.ShowSent) {
for (var i in self.Owners) {
if (ownerFilter === self.Owners[i].Username) {
visible = true;
break;
}
}
}
}
this.Visible(visible);
};
this.PackageIconUrlFallback = ko.pureComputed(function () {
var url = this.OwnerRequestsListViewModel.ManagePackagesViewModel.PackageIconUrlFallback;
return "this.src='" + url + "'; this.onerror = null;";
}, this);
}

function OwnerRequestsListViewModel(managePackagesViewModel, requests, showReceived, showSent) {
var self = this;

this.ManagePackagesViewModel = managePackagesViewModel;
this.Requests = $.map(requests, function (data) {
return new OwnerRequestsItemViewModel(self, data, showReceived, showSent);
});
this.VisibleRequestsCount = ko.observable();
this.VisibleRequestsHeading = ko.pureComputed(function () {
return formatOwnerRequestsData(ko.unwrap(self.VisibleRequestsCount()));
}, this);

this.ManagePackagesViewModel.OwnerFilter.subscribe(function (newOwner) {
var requestsCount = 0;
for (var i in self.Requests) {
self.Requests[i].UpdateVisibility(newOwner.Username);
if (self.Requests[i].Visible()) {
requestsCount++;
}
}
this.VisibleRequestsCount(requestsCount);
}, this);
}

function ManagePackagesViewModel(initialData) {
var self = this;

Expand All @@ -105,11 +240,17 @@

this.ListedPackages = new PackagesListViewModel(this, "published", initialData.ListedPackages);
this.UnlistedPackages = new PackagesListViewModel(this, "unlisted", initialData.UnlistedPackages);
this.ReservedNamespaces = new ReservedNamespaceListViewModel(this, initialData.ReservedNamespaces);
this.RequestsReceived = new OwnerRequestsListViewModel(this, initialData.RequestsReceived, true, false);
this.RequestsSent = new OwnerRequestsListViewModel(this, initialData.RequestsSent, false, true);
}

// Immediately load initial expander data
showInitialPackagesData("#listed-data", initialData.ListedPackages);
showInitialPackagesData("#unlisted-data", initialData.UnlistedPackages);
showInitialReservedNamespaceData("#namespaces-data", initialData.ReservedNamespaces);
showInitialOwnerRequestsData("#requests-received-data", initialData.RequestsReceived);
showInitialOwnerRequestsData("#requests-sent-data", initialData.RequestsSent);
Copy link
Member

Choose a reason for hiding this comment

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

Looks great - I know this was a lot of work!


// Set up the data binding.
var managePackagesViewModel = new ManagePackagesViewModel(initialData);
Expand Down
88 changes: 81 additions & 7 deletions src/NuGetGallery/UrlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,22 @@ public static string Register(this UrlHelper url, bool relativeUrl = true)
return GetActionLink(url, "LogOn", "Authentication", relativeUrl);
}

public static RouteUrlTemplate<string> SearchTemplate(this UrlHelper url, bool relativeUrl = true)
{
var routesGenerator = new Dictionary<string, Func<string, object>>
{
{ "q", s => s }
};

Func<RouteValueDictionary, string> linkGenerator = rv => GetRouteLink(
url,
RouteName.ListPackages,
relativeUrl,
routeValues: rv);

return new RouteUrlTemplate<string>(linkGenerator, routesGenerator);
}

public static string Search(this UrlHelper url, string searchTerm, bool relativeUrl = true)
{
return GetRouteLink(
Expand Down Expand Up @@ -865,25 +881,63 @@ public static string RemovePackageOwner(this UrlHelper url, bool relativeUrl = t
return GetActionLink(url, "RemovePackageOwner", "JsonApi", relativeUrl);
}

public static RouteUrlTemplate<OwnerRequestsListItemViewModel> ConfirmPendingOwnershipRequestTemplate(
this UrlHelper url, bool relativeUrl = true)
{
return HandlePendingOwnershipRequestTemplate(url, RouteName.ConfirmPendingOwnershipRequest, relativeUrl);
}

public static string ConfirmPendingOwnershipRequest(
this UrlHelper url,
string packageId,
string username,
string confirmationCode,
bool relativeUrl = true)
{
var routeValues = new RouteValueDictionary
return HandlePendingOwnershipRequest(url, RouteName.ConfirmPendingOwnershipRequest, packageId, username, confirmationCode, relativeUrl);
}

public static RouteUrlTemplate<OwnerRequestsListItemViewModel> RejectPendingOwnershipRequestTemplate(
this UrlHelper url, bool relativeUrl = true)
{
return HandlePendingOwnershipRequestTemplate(url, RouteName.RejectPendingOwnershipRequest, relativeUrl);
}

public static string RejectPendingOwnershipRequest(
this UrlHelper url,
string packageId,
string username,
string confirmationCode,
bool relativeUrl = true)
{
return HandlePendingOwnershipRequest(url, RouteName.RejectPendingOwnershipRequest, packageId, username, confirmationCode, relativeUrl);
}

private static RouteUrlTemplate<OwnerRequestsListItemViewModel> HandlePendingOwnershipRequestTemplate(
this UrlHelper url,
string routeName,
bool relativeUrl = true)
{
var routesGenerator = new Dictionary<string, Func<OwnerRequestsListItemViewModel, object>>
{
["id"] = packageId,
["username"] = username,
["token"] = confirmationCode
{ "id", r => r.Package.Id },
{ "username", r => r.Request.NewOwner.Username },
{ "token", r => r.Request.ConfirmationCode }
};

return GetActionLink(url, "ConfirmPendingOwnershipRequest", "Packages", relativeUrl, routeValues);
Func<RouteValueDictionary, string> linkGenerator = rv => GetActionLink(
url,
routeName,
"Packages",
relativeUrl,
routeValues: rv);

return new RouteUrlTemplate<OwnerRequestsListItemViewModel>(linkGenerator, routesGenerator);
}

public static string RejectPendingOwnershipRequest(
private static string HandlePendingOwnershipRequest(
this UrlHelper url,
string routeName,
string packageId,
string username,
string confirmationCode,
Expand All @@ -896,7 +950,27 @@ public static string RejectPendingOwnershipRequest(
["token"] = confirmationCode
};

return GetActionLink(url, "RejectPendingOwnershipRequest", "Packages", relativeUrl, routeValues);
return GetActionLink(url, routeName, "Packages", relativeUrl, routeValues);
}

public static RouteUrlTemplate<OwnerRequestsListItemViewModel> CancelPendingOwnershipRequestTemplate(
this UrlHelper url, bool relativeUrl = true)
{
var routesGenerator = new Dictionary<string, Func<OwnerRequestsListItemViewModel, object>>
{
{ "id", r => r.Package.Id },
{ "requestingUsername", r => r.Request.RequestingOwner.Username },
{ "pendingUsername", r => r.Request.NewOwner.Username }
};

Func<RouteValueDictionary, string> linkGenerator = rv => GetActionLink(
url,
"CancelPendingOwnershipRequest",
"Packages",
relativeUrl,
routeValues: rv);

return new RouteUrlTemplate<OwnerRequestsListItemViewModel>(linkGenerator, routesGenerator);
}

public static string CancelPendingOwnershipRequest(
Expand Down
Loading