Skip to content
Open
7 changes: 7 additions & 0 deletions civictechprojects/helpers/context_preload.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ def my_groups_preload(context, request):
context['description'] = 'My Groups page'
return context

def group_projects_preload(context, request):
context = default_preload(context, request)
context['title'] = 'Participating Projects | DemocracyLab'
context['description'] = 'Projects that the group participate'
return context

def find_events_preload(context, request):
context = default_preload(context, request)
context['title'] = 'Find Events | DemocracyLab'
Expand Down Expand Up @@ -229,6 +235,7 @@ def default_preload(context, request):
{'section': FrontEndSection.CreateProject.value,'handler':create_project_preload},
{'section': FrontEndSection.MyProjects.value, 'handler': my_projects_preload},
{'section': FrontEndSection.MyGroups.value, 'handler': my_groups_preload},
{'section': FrontEndSection.GroupProjects.value, 'handler': group_projects_preload},
{'section': FrontEndSection.MyEvents.value, 'handler': my_events_preload},
{'section': FrontEndSection.Donate.value, 'handler': donate_preload},
{'section': FrontEndSection.AboutGroup.value, 'handler': about_group_preload},
Expand Down
2 changes: 1 addition & 1 deletion civictechprojects/helpers/search/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,6 @@ def get_tag_counts(category=None, event=None, group=None):
for slug in querydict.keys():
resultdict[slug] = Tag.hydrate_tag_model(querydict[slug])
resultdict[slug]["num_times"] = (
activetagdict[slug] if slug in activetagdict else 0
activetagdict[slug] if activetagdict and slug in activetagdict else 0
)
return list(resultdict.values())
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.ConfirmRemoveGroupProjectModal-buttons{
display: flex;
flex-direction: column;
width: 100%;
gap: 12px;
}
53 changes: 53 additions & 0 deletions civictechprojects/static/css/partials/_GroupProjectCard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.GroupProjectCard-root {
color: inherit;
background-color: $color-background-light;
border: solid 1px $color-grey-frame-border;
margin: 20px auto;
padding: 16px;
}

.GroupProjectCard-header {
color: $color-text-dark;
font-style: italic;
}

.GroupProjectCard-projectName {
font-weight: bold;
}

.GroupProjectCard-item{
margin-top: 10px;
}
.GroupProjectCard-button-container{
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.GroupProjectCard-button {
margin: 5px;
}

.GroupProjectCard-viewButton{
margin-left: 0;
}

.GroupProjectCard-table td {
display: inline-block;
width: 100%;
}

@include media-breakpoint-up(sm) {
.GroupProjectCard-table td {
display: table-cell;
width: 250px;
}
.GroupProjectCard-item{
margin-top: 0;
}
.GroupProjectCard-viewButton{
margin-left: 5;
}
.GroupProjectCard-button-container{
justify-content: flex-end;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.GroupProjectController-root {
background: $color-background-default;
h3 {
margin-top: 24px;
}
}
.GroupProjectController-header{
display: flex;
flex-direction: row;
align-items: flex-end;
}
.GroupProjectController-return-group-button{
margin-left: auto;
margin-bottom: 8px;
cursor: pointer;
}
13 changes: 12 additions & 1 deletion civictechprojects/static/css/partials/_MyProjectCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
padding: 16px;
}

.MyProjectCard-groupButtons{
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
}

.MyProjectCard-header {
color: $color-text-dark;
font-style: italic;
Expand All @@ -28,5 +34,10 @@
.MyProjectCard-table td {
display: table-cell;
width: 250px;
}
}
}
@media only screen and (max-width: 1440px) {
.MyProjectCard-groupButtons{
justify-content: flex-start;
}
}
8 changes: 7 additions & 1 deletion civictechprojects/static/css/partials/_Profile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
margin-left: auto;
align-items: flex-end;
}

.Profile-owner-button-container{
display: flex;
flex-direction: column;
gap: 12px;
flex-wrap: wrap;
align-items: flex-end;
}
.Profile-top-details {
flex-basis: 100%;
order: 3;
Expand Down
10 changes: 10 additions & 0 deletions civictechprojects/static/css/partials/_Toasts.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@
.toast {
max-width: 400px; // this is a test of slightly wider toasts @ desktop viewports
}
}

/* The animation code */
@keyframes slidein {
from {translate: 100vw;}
to {translate: 0%;}
}
/* The element to apply the animation to */
.Toast-animation {
animation: 0.3s linear slidein;
}
3 changes: 3 additions & 0 deletions civictechprojects/static/css/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
@import "partials/SponsorFooter";
@import "partials/MyProjectCard";
@import "partials/MyProjectsController";
@import "partials/GroupProjectCard";
@import "partials/GroupProjectsController";
@import "partials/ProjectCard";
@import "partials/ProjectCardsContainer";
@import "partials/ProjectCardContainer";
Expand Down Expand Up @@ -100,6 +102,7 @@
@import "partials/Forms";
@import "partials/AboutProjectEventDisplay";
@import "partials/Toasts";
@import "partials/ConfirmRemoveGroupProjectModal";
@import "partials/JoinConference";
@import "partials/Terms";
@import "partials/Privacy";
Expand Down
38 changes: 37 additions & 1 deletion civictechprojects/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.urls import reverse
from django.utils.timezone import now

from civictechprojects.models import Group, Project
from civictechprojects.models import Group, Project, ProjectRelationship
from democracylab.models import Contributor


Expand Down Expand Up @@ -213,3 +213,39 @@ def test_api_views_throttling__project_delete(self):
expect_succeeded = 10 if is_authenticated else 5
self.assertEqual(num_succeeded, expect_succeeded)
self.assertEqual(num_throttled, 12-expect_succeeded)

def test_api_views_throttling__group_project_remove(self):
for is_authenticated in {True, False}:
client = Client()
if is_authenticated:
client.force_login(self.test_user)
group = Group.objects.create(
group_creator=self.test_user,
group_name='test-name',
is_searchable=True,
)
projects = Project.objects.bulk_create([
Project(
project_creator=self.test_user,
project_name=f'test-name-{i}',
is_searchable=True,
) for i in range(12)
])
# add project to group
relationshipProjects = ProjectRelationship.objects.bulk_create([
ProjectRelationship(
relationship_group=group,
relationship_project=project,
is_approved=True,
) for project in projects
])
num_succeeded, num_throttled = self.make_many_requests(
client=client,
method='post',
params=[{
'path': reverse('group_project_remove', kwargs={'project_id': project.id,'group_id':group.id}),
} for project in projects],
)
expect_succeeded = 10 if is_authenticated else 5
self.assertEqual(num_succeeded, expect_succeeded)
self.assertEqual(num_throttled, 12-expect_succeeded)
1 change: 1 addition & 0 deletions civictechprojects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
re_path(r'^api/groups/approve/(?P<group_id>[0-9]+)/$', views.approve_group, name='approve_group'),
re_path(r'^api/groups/edit/(?P<group_id>[0-9]+)/$', views.group_edit, name='group_edit'),
re_path(r'^api/groups/delete/(?P<group_id>[0-9]+)/$', views.group_delete, name='group_delete'),
re_path(r'^api/groups/(?P<group_id>.*)/projects/remove/(?P<project_id>.*)/$', views.group_project_remove, name='group_project_remove'),
re_path(r'^api/event/(?P<event_id>.*)/projects/(?P<project_id>.*)/create/$', views.event_project_edit, name='event_project_edit'),
re_path(r'^api/event/(?P<event_id>.*)/projects/(?P<project_id>.*)/rsvp/$', views.rsvp_for_event_project, name='rsvp_for_event_project'),
re_path(r'^api/event/(?P<event_id>.*)/projects/(?P<project_id>.*)/cancel/$', views.cancel_rsvp_for_event_project, name='cancel_rsvp_for_event_project'),
Expand Down
31 changes: 30 additions & 1 deletion civictechprojects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from democracylab.models import Contributor, get_request_contributor
from common.models.tags import Tag
from common.helpers.constants import FrontEndSection, TagCategory
from democracylab.emails import send_to_project_owners, send_to_project_volunteer, HtmlEmailTemplate, send_volunteer_application_email, \
from democracylab.emails import send_to_project_owners, send_to_project_volunteer,Html, HtmlEmailTemplate, send_volunteer_application_email, \
send_volunteer_conclude_email, notify_project_owners_volunteer_renewed_email, notify_project_owners_volunteer_concluded_email, \
notify_project_owners_project_approved, contact_democracylab_email, send_to_group_owners, send_group_project_invitation_email, \
notify_group_owners_group_approved, notify_event_owners_event_approved, notify_rsvped_volunteer, notify_rsvp_cancellation, \
Expand Down Expand Up @@ -118,6 +118,35 @@ def group_delete(request, group_id):
return HttpResponseForbidden()
return HttpResponse(status=204)

@api_view(['POST'])
def group_project_remove(request, group_id,project_id):
user = request.user
message = request.data.get('message',"")
group = Group.objects.get(id=group_id)
project = Project.objects.get(id=project_id)
if not user.is_authenticated:
return HttpResponse(status=401)
# only creator or staff can remove the project
if not is_creator(user, group) and not user.is_staff:
return HttpResponse(status=403)
# Remove project relationship
project_relation = ProjectRelationship.objects.get(relationship_project=project_id)
project_relation.delete()
project.recache()
group.recache()
group.update_timestamp()
# set up mail
link_to_group = section_url(FrontEndSection.AboutGroup,{'id':group_id})
link_to_project = section_url(FrontEndSection.AboutProject,{'id': project_id})
email_subject = '{} has been removed from collaboration'.format(project.project_name)
email_template = HtmlEmailTemplate(use_signature=False)
email_template.header('{} has been removed from collaboration'.format(project.project_name))
email_template.paragraph(Html.a(href=link_to_group,text=group.group_name) + ' has removed '+Html.a(href=link_to_project,text=project.project_name)+' from their group. If you have any questions, please contact the group owner via link below.')
if(len(message)>0):
email_template.paragraph('"{}"'.format(message))
email_template.button(link_to_group,"CONTACT GROUP",text_color="#000000",text_decoration='none')
send_to_project_owners(project=project,sender=user,subject=email_subject,template=email_template,include_co_owners=False)
return HttpResponse(status=204)

@api_view()
def get_group(request, group_id):
Expand Down
15 changes: 10 additions & 5 deletions common/components/common/ModalWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ type Props = {|
hideButtons: ?boolean,
size: ?string,
reverseCancelConfirm: ?boolean,
cancelButtonVariant: ?string,
submitButtonVariant: ?string,
buttons: ?React$Node,
|};
type State = {||};

Expand Down Expand Up @@ -57,9 +60,9 @@ class ModalWrapper extends React.PureComponent<Props, State> {
<Modal.Title>{this.props.headerText}</Modal.Title>
</Modal.Header>
<Modal.Body>{this.props.children}</Modal.Body>
{!this.props.hideButtons && (
<Modal.Footer>
{this.props.reverseCancelConfirm ? (
{!this.props.hideButtons && (this.props.buttons)? this.props.buttons : (
this.props.reverseCancelConfirm ? (
<React.Fragment>
{this._renderSubmitButton()}
{this.props.onClickCancel && this._renderCancelButton()}
Expand All @@ -69,18 +72,19 @@ class ModalWrapper extends React.PureComponent<Props, State> {
{this.props.onClickCancel && this._renderCancelButton()}
{this._renderSubmitButton()}
</React.Fragment>
)
)}
</Modal.Footer>
)}
</Modal>
</div>
);
}

_renderCancelButton(): React$Node {
const {cancelButtonVariant="outline-secondary"} = this.props;
return (
<Button
variant="outline-secondary"
variant={cancelButtonVariant}
onClick={() => this.props.onClickCancel()}
disabled={!this.props.cancelEnabled}
>
Expand All @@ -90,6 +94,7 @@ class ModalWrapper extends React.PureComponent<Props, State> {
}

_renderSubmitButton(): React$Node {
const {submitButtonVariant="primary"} = this.props;
// TODO: Figure out more visually pleasing spinner solution
const buttonContent: React$Node = this.props.submitText ? (
<React.Fragment>{this.props.submitText}</React.Fragment>
Expand All @@ -98,7 +103,7 @@ class ModalWrapper extends React.PureComponent<Props, State> {
);
return (
<Button
variant="primary"
variant={submitButtonVariant}
disabled={!this.props.submitEnabled}
onClick={() => this.props.onClickSubmit()}
>
Expand Down
21 changes: 18 additions & 3 deletions common/components/common/groups/ContactGroupButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ class ContactGroupButton extends React.PureComponent<Props, State> {
);
}

//change href to manage page
_renderManageProjectsButton(): React$Node {
const id = { id: this.props.group.group_id };
return (
<Button
variant="primary"
disabled={this.state.buttonDisabled}
title={this.state.buttonTitle}
href={url.section(Section.GroupProjects, id)}
>
Manage Projects
</Button>
);
}

_renderContactGroupButton(): React$Node {
return (
<Button
Expand All @@ -137,13 +152,13 @@ class ContactGroupButton extends React.PureComponent<Props, State> {
);
}

displayEditGroupButton(): ?React$Node {
displayOwnerButtons(): ?React$Node {
if (
CurrentUser.userID() === this.props.group.group_creator ||
CurrentUser.isCoOwner(this.props.group) ||
CurrentUser.isStaff()
) {
return <div>{this._renderEditGroupButton()}</div>;
return <div className="Profile-owner-button-container"><div>{this._renderEditGroupButton()}</div><div>{this._renderManageProjectsButton()}</div></div>;
}
}

Expand Down Expand Up @@ -172,7 +187,7 @@ class ContactGroupButton extends React.PureComponent<Props, State> {
if (CurrentUser.isLoggedIn()) {
return (
<div>
{this.displayEditGroupButton()}
{this.displayOwnerButtons()}
{this.displayContactGroupButton()}
</div>
);
Expand Down
Loading