Skip to content

Commit

Permalink
Show related projects in same org when creating project (#979)
Browse files Browse the repository at this point in the history
When the user selects an org in the create-project page, any projects
with similar names in that org are shown in a list of possibly-related
projects, and the user is invited to choose one of them to join instead
of creating a new project. If he chooses to join the project, an email
is sent to that project's manager(s) to ask them to approve it. Further
down the create-project form, when a language code is entered, another
GraphQL query is made searching for any projects with a vernacular
writing system matching that language code, and those projects are added
to the related-projects list. (Ordered first, above the related-by-name
results, because they're more likely to be correct).

The related-projects list replaces the project description field and the
submit button, so the user must interact with it, either by clicking on
"Yes, join project" or by clicking on "No thanks, create new project"
before he can move on to creating the project. If he dismisses it by
clicking "No thanks", he can still change his mind and see the list of
related projects again.

When the project manager receives the email saying "(name) wants to join
the project", he can click on the button to approve it. That button will
take him to the project page, with the "Add member" modal already filled
in with the new user's name and (invisibly) GUID. He can then select a
role, either editor or manager, for the new user, and click on Add.
  • Loading branch information
rmunn authored Aug 9, 2024
1 parent 7cf4ee2 commit 7630ed0
Show file tree
Hide file tree
Showing 18 changed files with 870 additions and 35 deletions.
44 changes: 44 additions & 0 deletions backend/LexBoxApi/GraphQL/LexQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,50 @@ public IQueryable<DraftProject> DraftProjects(LexBoxDbContext context)
return context.DraftProjects;
}

public record ProjectsByLangCodeAndOrgInput(Guid OrgId, string LangCode);
[UseProjection]
[UseSorting]
public IQueryable<Project> ProjectsByLangCodeAndOrg(LoggedInContext loggedInContext, LexBoxDbContext context, IPermissionService permissionService, ProjectsByLangCodeAndOrgInput input)
{
if (!loggedInContext.User.IsAdmin && !permissionService.IsOrgMember(input.OrgId)) throw new UnauthorizedAccessException();
// Convert 3-letter code to 2-letter code if relevant, otherwise leave as-is
var langCode = Services.LangTagConstants.ThreeToTwo.GetValueOrDefault(input.LangCode, input.LangCode);
var query = context.Projects.Where(p =>
p.Organizations.Any(o => o.Id == input.OrgId) &&
p.FlexProjectMetadata != null &&
p.FlexProjectMetadata.WritingSystems != null &&
p.FlexProjectMetadata.WritingSystems.VernacularWss.Any(ws =>
ws.IsActive && (
ws.Tag == langCode ||
ws.Tag == $"qaa-x-{langCode}" ||
ws.Tag.StartsWith($"{langCode}-")
)
)
);
// Org admins can see all projects, everyone else can only see non-confidential
if (!permissionService.CanEditOrg(input.OrgId))
{
query = query.Where(p => p.IsConfidential == false);
}
return query;
}

public record ProjectsInMyOrgInput(Guid OrgId);
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Project> ProjectsInMyOrg(LoggedInContext loggedInContext, LexBoxDbContext context, IPermissionService permissionService, ProjectsInMyOrgInput input)
{
if (!loggedInContext.User.IsAdmin && !permissionService.IsOrgMember(input.OrgId)) throw new UnauthorizedAccessException();
var query = context.Projects.Where(p => p.Organizations.Any(o => o.Id == input.OrgId));
// Org admins can see all projects, everyone else can only see non-confidential
if (!permissionService.CanEditOrg(input.OrgId))
{
query = query.Where(p => p.IsConfidential == false);
}
return query;
}

[UseSingleOrDefault]
[UseProjection]
public async Task<IQueryable<Project>> ProjectById(LexBoxDbContext context, IPermissionService permissionService, Guid projectId)
Expand Down
36 changes: 36 additions & 0 deletions backend/LexBoxApi/GraphQL/ProjectMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,42 @@ await dbContext.ProjectUsers
return dbContext.ProjectUsers.Where(u => u.Id == projectUser.Id);
}

[Error<NotFoundException>]
[Error<DbError>]
[Error<ProjectMembersMustBeVerified>]
[Error<ProjectMembersMustBeVerifiedForRole>]
[UseMutationConvention]
[UseFirstOrDefault]
[UseProjection]
public async Task<IQueryable<Project>> AskToJoinProject(
IPermissionService permissionService,
LoggedInContext loggedInContext,
Guid projectId,
LexBoxDbContext dbContext,
[Service] IEmailService emailService)
{
await permissionService.AssertCanAskToJoinProject(projectId);

var user = await dbContext.Users.FindAsync(loggedInContext.User.Id);
if (user is null) throw new UnauthorizedAccessException();
user.AssertHasVerifiedEmailForRole(ProjectRole.Editor);

var project = await dbContext.Projects
.Include(p => p.Users)
.ThenInclude(u => u.User)
.Where(p => p.Id == projectId)
.FirstOrDefaultAsync();
NotFoundException.ThrowIfNull(project);

var managers = project.Users.Where(u => u.Role == ProjectRole.Manager);
foreach (var manager in managers)
{
if (manager.User is null) continue;
await emailService.SendJoinProjectRequestEmail(manager.User, user, project);
}
return dbContext.Projects.Where(p => p.Id == projectId);
}

[Error<NotFoundException>]
[Error<DbError>]
[Error<RequiredException>]
Expand Down
2 changes: 2 additions & 0 deletions backend/LexBoxApi/Services/Email/EmailTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum EmailTemplate
PasswordChanged,
CreateAccountRequestProject,
CreateAccountRequestOrg,
JoinProjectRequest,
CreateProjectRequest,
ApproveProjectRequest,
UserAdded,
Expand All @@ -35,6 +36,7 @@ public record OrgInviteEmail(string Email, string ManagerName, string OrgName, s

public record PasswordChangedEmail(string Name) : EmailTemplateBase(EmailTemplate.PasswordChanged);

public record JoinProjectRequestEmail(string ManagerName, string RequestingUserName, Guid requestingUserId, string ProjectCode, string ProjectName) : EmailTemplateBase(EmailTemplate.JoinProjectRequest);
public record CreateProjectRequestUser(string Name, string Email);
public record CreateProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project) : EmailTemplateBase(EmailTemplate.CreateProjectRequest);
public record ApproveProjectRequestEmail(string Name, CreateProjectRequestUser User, CreateProjectInput Project) : EmailTemplateBase(EmailTemplate.ApproveProjectRequest);
Expand Down
1 change: 1 addition & 0 deletions backend/LexBoxApi/Services/Email/IEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public Task SendCreateAccountWithProjectEmail(

public Task SendPasswordChangedEmail(User user);

public Task SendJoinProjectRequestEmail(User projectManagers, User requestingUser, Project project);
public Task SendCreateProjectRequestEmail(LexAuthUser user, CreateProjectInput projectInput);
public Task SendApproveProjectRequestEmail(User user, CreateProjectInput projectInput);
public Task SendUserAddedEmail(User user, string projectName, string projectCode);
Expand Down
7 changes: 7 additions & 0 deletions backend/LexBoxApi/Services/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ public async Task SendPasswordChangedEmail(User user)
await SendEmailWithRetriesAsync(email);
}

public async Task SendJoinProjectRequestEmail(User projectManager, User requestingUser, Project project)
{
var email = StartUserEmail(projectManager) ?? throw new ArgumentNullException("emailAddress");
await RenderEmail(email, new JoinProjectRequestEmail(projectManager.Name, requestingUser.Name, requestingUser.Id, project.Code, project.Name), projectManager.LocalizationCode);
await SendEmailWithRetriesAsync(email);
}

public async Task SendCreateProjectRequestEmail(LexAuthUser user, CreateProjectInput projectInput)
{
var email = new MimeMessage();
Expand Down
Loading

0 comments on commit 7630ed0

Please sign in to comment.