User Module: Filter Users on Role

Topics: Core
Jun 20, 2013 at 2:23 AM
Edited Jun 26, 2013 at 5:20 AM
So, I brought this up as a feature request, but discussions are probably a better spot to convey my idea.

ScreenshotUserAdmin

Sebastien said it can't add any dependency from the Users module, so I had a look to the Workflows module to see how the Activities are implemented - pretty easy.

However I have come across two issues which I could use some help with (either by suggestions for alternative approaches, or how they could be implemented):

Issue 1: Accessing the UserPart from UserPartRecord:

In the Users controller, the users are sorted and filtered already. I simply wanted to add an extra filter to be applied. However, unlike the current filter, we don't know which query predicate to apply in the controller. So I wanted to make the Filter implement the predicate:

AdminController.cs
...
if (!String.IsNullOrWhiteSpace(options.UserFilter)) {
    var userFilter = _userFiltersManager.GetFilterByName(options.UserFilter);
    users = users.Where(userFilter.Filter);
}
...
Then, with the particular filter which we're using (which is set in Roles):
AdministratorRoleFilter.cs
namespace Orchard.Roles.Filters {
    public class AdministratorRoleFilter : IUserRoleFilter {
        public string Name { 
            get { return "Administrator"; }
        }

        public Expression<Func<UserPartRecord, bool>> Filter {
            get {
                // Need to apply filter here
                return record => true;
            }
        }
    }
}
But, as you can see, at this point we only have access to UserPartRecord. I think I need to get the UserPart so I can do something like:
var roles = userPart.As<IUserRoles>().Roles;
// Check if user has role for this filter...
Issue 2: The second issue, and this is the bigger issue, is that the method above I'm manually implementing the IUserRoleFilter interface; which makes the whole thing useless for obvious reasons.

Is it possible to dynamically create these filters at run time? So when Orchard scans for all the IUserRoleFilter interfaces, the Roles module gets its Roles and creates the Filters on the fly.

To be clear, I haven't considered performance issues at all either, but I can imagine that may be a factor also.

I'm open to ideas, alternative solutions, or any hints to point me in the right direction. I'm still learning a lot about Orchard as I only started developing modules a few months ago, but I figured the best way to learn is to dive in and have a go.
Coordinator
Jun 21, 2013 at 1:26 AM
Issue 1: you can't get directly from record to part, only from part to record. What you can do is fetch the content item from the content manager using the id that is on the record, and then As<ThePartType>(). Or query on the records that you need, using Joins, and do your filtering on those records.

I don't understand issue 2.
Jun 21, 2013 at 1:48 AM
Thanks Bertrand.

Issue 2:
This one is a little difficult to explain, but I know once it clicks you'll understand what I'm trying to do.

Using the Workflows module as an example (to make explaining "easier"); the Activities displayed in the Workflows module Dashboard are defined by classes implementing the IActivity interface. Then Orchard's magic (either by Event Bus or however it works) gets all the class definitions implementing the interface and viola, it all works great. Rules and Tokens use the same method I believe.

So, whatever that's called, that's what I've set up in the Users module to load up Filters. I created an IUserRoleFilter interface in the Users module, and just a manager to handle it (copied from the Workflows module); and then in the Roles module, I manually wrote a Filter (by implementing IUserRoleFilter) for 'Administrator' to test that the filtering works; which it did!

The issue is, the Roles aren't hard coded in Orchard. So I can't manually define the Filters.

My question is: Is it possible to dynamically create the class definitions implementing the IUserRoleFilter interface at run time?

My secondary question is: Is this even the best approach? Is there a better approach? What alternatives are there?
Coordinator
Jun 26, 2013 at 4:54 AM
I still don't understand. What could you mean by "dynamically create the class definitions"? I also don't get what you mean by "manually define the Filters"? What filters, and what would need to be "manually defined"?
Jun 26, 2013 at 5:16 AM
Edited Jun 26, 2013 at 5:20 AM
When I say "manually defined", I mean physically writing the code. In context, when I say manually defining a filter, I mean creating a file like this:
namespace Orchard.Roles.Filters {
    public class AdministratorRoleFilter : IUserRoleFilter {
        public string Name { 
            get { return "Administrator"; }
        }

        public Expression<Func<UserPartRecord, bool>> Filter {
            get {
                // Need to apply filter here
                return record => true;
            }
        }
    }
}
Which creates a UserRoleFilter from this interface:
namespace Orchard.Users.Services {
    public interface IUserRoleFilter : IDependency {
        string Name { get; }
        Expression<Func<UserPartRecord, bool>> Filter { get; }
    }
}
...which is to populate this dropdown:

Image

...with this code in the controller:
...
if (!String.IsNullOrWhiteSpace(options.UserFilter)) {
    var userFilter = _userFiltersManager.GetFilterByName(options.UserFilter);
    users = users.Where(userFilter.Filter);
}
...
...which uses a manager with this code:
namespace Orchard.Users.Services {
    public class UserRoleFiltersManager : IUserRoleFiltersManager {
        private readonly IEnumerable<IUserRoleFilter> _userFilters;

        public UserRoleFiltersManager(IEnumerable<IUserRoleFilter> userFilters) {
            _userFilters = userFilters;
        }

        public IEnumerable<IUserRoleFilter> GetFilters() {
            return _userFilters.OrderBy(x => x.Name).ToReadOnlyCollection();
        }

        public IUserRoleFilter GetFilterByName(string name) {
            return _userFilters.FirstOrDefault(x => x.Name == name);
        }
    }
}
So, the UserRoleFiltersManager gets all classes implementing the IUserRoleFilter interface, which then populates a drop down on the User Dashboard to provide an extra filter for filtering the users (based on Roles).

The Issue

Roles aren't always defined in code. They can be created via the dashboard, which would mean a "filter" for those particular roles wouldn't appear in the dropdown because a "filter" wouldn't have been defined in code.

What I Want To Do

Find a way to "create" these files, or at least the classes, dynamically at run time. So that a user could log on, create a custom user role, and then view a filtered list of all users with that custom role.
Coordinator
Jun 26, 2013 at 5:23 AM
So the problem is that you need to use roles (because you want to filter on roles) but you want to do it from the Users module, which can't depend on roles? I think you're taking this from completely the wrong end. This just means that the users module should have an extensibility point for other modules to inject filters and stuff. Then, the roles filter should be added by the roles module.
Jun 26, 2013 at 5:28 AM
BertrandLeRoy wrote:
So the problem is that you need to use roles (because you want to filter on roles) but you want to do it from the Users module, which can't depend on roles? I think you're taking this from completely the wrong end. This just means that the users module should have an extensibility point for other modules to inject filters and stuff. Then, the roles filter should be added by the roles module.
Yep.

I think I have kind of done that, but I probably haven't done the injection side of things correctly. I forked the repo to have a crack at it. As I said, the best way to learn is jump in and have a go. But I'm open to ideas and constructive criticism. I don't have such a deep understanding that I can instantly recognise a bad idea when I see one haha.

I'd love to be steered in the right direction!
Coordinator
Jun 26, 2013 at 5:34 AM
I understand. Thanks for that. However, I don't see your changes in that fork. Did you push them?
Jun 26, 2013 at 5:41 AM
It was my first time forking a project like this, so I created a new branch. This is the changeset.
Coordinator
Jun 26, 2013 at 5:57 AM
Thanks. It makes a world of difference to be able to see the changeset in its entirety. Now I understand.

I don't think IUserRoleFilter and IUserRoleFiltersManager should be called that. The term "Role" should not be present in the name as that implies that it can only be used for roles. I'd go for IUserFilter and IUserFilterProvider.

This seems overly complex. The expression doesn't seem to be necessary, especially as expressions are not trivial to generate (they are not super-hard, but by using them, you are raising the bar about who can write one of those filters). Here is what I would do instead.

Inject an IEnumerable<IUserFilterProvider> instead of just an IUserFilterProvider. In Roles, you would have one implementation that would return a list of IUserFilter from GetFilters. Each of those filters are instances of a specific implementation of IUserFilter done in the Roles module that has the role name as a property. Filter should be a method, not a property, with signature bool Filter(UserPartRecord user).

When getting the full list of IUserFilters from the list of IUserFilterProvider, remember that SelectMany can be a huge help.

Does this make sense?
Jun 26, 2013 at 6:03 AM
I think so. When I next open it up I'll have a look at your suggestions and how I might be able to implement them. I do have a tendency to go a little overboard with complexity at times. Still learning to reign it in.

Thanks!

I think it would be a useful feature for Orchard, as such I made the request, but I figured I'd have a crack at it myself since Orchard is an open-source project.
Coordinator
Jun 26, 2013 at 6:04 AM
Yes, thanks again for giving it a shot.
Jul 2, 2013 at 11:18 PM
I haven't forgotten about this one - but tomorrow I will leave for a 5 week holiday to Cape York. No internet access out there. Anybody else is welcome to have a crack based on Bertrand's suggestions! Otherwise I will take another look when I get back :)