Skip to content

Commit

Permalink
Merge pull request #4049 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.2.4 Release
  • Loading branch information
willemarcel committed Jan 5, 2021
2 parents b2847dc + 96241ae commit 7ae5c70
Show file tree
Hide file tree
Showing 61 changed files with 1,481 additions and 500 deletions.
15 changes: 3 additions & 12 deletions backend/models/dtos/project_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,6 @@
)
from backend.models.dtos.campaign_dto import CampaignDTO

ORDER_BY_OPTIONS = (
"id",
"mapper_level",
"priority",
"status",
"last_updated",
"due_date",
)


def is_known_project_status(value):
""" Validates that Project Status is known value """
Expand Down Expand Up @@ -289,16 +280,16 @@ class ProjectSearchDTO(Model):

preferred_locale = StringType(default="en")
mapper_level = StringType(validators=[is_known_mapping_level])
action = StringType(choices=("map", "validate", "any"))
action = StringType()
mapping_types = ListType(StringType, validators=[is_known_mapping_type])
mapping_types_exact = BooleanType(required=False)
project_statuses = ListType(StringType, validators=[is_known_project_status])
organisation_name = StringType()
organisation_id = IntType()
team_id = IntType()
campaign = StringType()
order_by = StringType(choices=ORDER_BY_OPTIONS)
order_by_type = StringType(choices=("ASC", "DESC"))
order_by = StringType()
order_by_type = StringType()
country = StringType()
page = IntType(required=True)
text_search = StringType()
Expand Down
2 changes: 1 addition & 1 deletion backend/models/postgis/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ class Task(db.Model):

# Mapped objects
task_history = db.relationship(
TaskHistory, cascade="all", order_by=desc(TaskHistory.id)
TaskHistory, cascade="all", order_by=desc(TaskHistory.action_date)
)
task_annotations = db.relationship(TaskAnnotation, cascade="all")
lock_holder = db.relationship(User, foreign_keys=[locked_by])
Expand Down
83 changes: 50 additions & 33 deletions backend/services/messaging/message_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def _push_messages(messages):
message["message"].id,
UserService.get_user_by_id(message["message"].from_user_id).username,
message["message"].project_id,
message["message"].task_id,
clean_html(message["message"].subject),
message["message"].message,
obj.message_type,
Expand Down Expand Up @@ -238,6 +239,7 @@ def send_message_after_comment(
user_from = User.query.get(comment_from)
if user_from is None:
raise ValueError("Username not found")
user_link = MessageService.get_user_link(user_from.username)

task_link = MessageService.get_task_link(project_id, task_id)
messages = []
Expand All @@ -257,7 +259,9 @@ def send_message_after_comment(
message.from_user_id = comment_from
message.task_id = task_id
message.to_user_id = user.id
message.subject = f"{user_from.username} left a comment in {task_link} of Project {project_id}"
message.subject = (
f"{user_link} left a comment in {task_link} of Project {project_id}"
)
message.message = comment
messages.append(dict(message=message, user=user))

Expand Down Expand Up @@ -375,55 +379,68 @@ def send_message_after_chat(chat_from: int, chat: str, project_id: int):
app = create_app()
with app.app_context():
usernames = MessageService._parse_message_for_username(chat, project_id)
if len(usernames) == 0:
return # Nobody @'d so return

link = MessageService.get_project_link(
project_id, include_chat_section=True
)

messages = []
for username in usernames:
current_app.logger.debug(f"Searching for {username}")
try:
user = UserService.get_user_by_username(username)
except NotFound:
current_app.logger.error(f"Username {username} not found")
continue # If we can't find the user, keep going no need to fail
if len(usernames) != 0:
link = MessageService.get_project_link(
project_id, include_chat_section=True
)
messages = []
for username in usernames:
current_app.logger.debug(f"Searching for {username}")
try:
user = UserService.get_user_by_username(username)
except NotFound:
current_app.logger.error(f"Username {username} not found")
continue # If we can't find the user, keep going no need to fail

message = Message()
message.message_type = MessageType.MENTION_NOTIFICATION.value
message.project_id = project_id
message.from_user_id = chat_from
message.to_user_id = user.id
message.subject = f"You were mentioned in {link} chat"
message.message = chat
messages.append(dict(message=message, user=user))
message = Message()
message.message_type = MessageType.MENTION_NOTIFICATION.value
message.project_id = project_id
message.from_user_id = chat_from
message.to_user_id = user.id
message.subject = f"You were mentioned in {link} chat"
message.message = chat
messages.append(dict(message=message, user=user))

MessageService._push_messages(messages)
MessageService._push_messages(messages)

query = """ select user_id from project_favorites where project_id = :project_id"""
result = db.engine.execute(text(query), project_id=project_id)
favorited_users = [r[0] for r in result]
favorited_users_results = db.engine.execute(
text(query), project_id=project_id
)
favorited_users = [r[0] for r in favorited_users_results]

# Notify all contributors except the user that created the comment.
contributed_users_results = (
TaskHistory.query.with_entities(TaskHistory.user_id.distinct())
.filter(TaskHistory.project_id == project_id)
.filter(TaskHistory.user_id != chat_from)
.filter(TaskHistory.action == TaskAction.STATE_CHANGE.name)
.all()
)
contributed_users = [r[0] for r in contributed_users_results]

users_to_notify = list(set(contributed_users + favorited_users))

if len(favorited_users) != 0:
if len(users_to_notify) != 0:
from_user = User.query.get(chat_from)
from_user_link = MessageService.get_user_link(from_user.username)
project_link = MessageService.get_project_link(
project_id, include_chat_section=True
)
# project_title = ProjectService.get_project_title(project_id)
messages = []
for user_id in favorited_users:

for user_id in users_to_notify:
try:
user = UserService.get_user_dto_by_id(user_id)
except NotFound:
continue # If we can't find the user, keep going no need to fail

message = Message()
message.message_type = MessageType.PROJECT_CHAT_NOTIFICATION.value
message.project_id = project_id
message.from_user_id = chat_from
message.to_user_id = user.id
message.subject = f"{chat_from} left a comment in {project_link}"
message.subject = (
f"{from_user_link} left a comment in {project_link}"
)
message.message = chat
messages.append(dict(message=message, user=user))

Expand Down
4 changes: 4 additions & 0 deletions backend/services/messaging/smtp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def send_email_alert(
message_id: int,
from_username: str,
project_id: int,
task_id: int,
subject: str,
content: str,
message_type: int,
Expand All @@ -60,6 +61,7 @@ def send_email_alert(
current_app.logger.debug(f"Test if email required {to_address}")
from_user_link = f"{current_app.config['APP_BASE_URL']}/users/{from_username}"
project_link = f"{current_app.config['APP_BASE_URL']}/projects/{project_id}"
task_link = f"{current_app.config['APP_BASE_URL']}/projects/{project_id}/tasks/?search={task_id}"
settings_url = "{}/settings#notifications".format(
current_app.config["APP_BASE_URL"]
)
Expand All @@ -76,6 +78,8 @@ def send_email_alert(
"FROM_USERNAME": from_username,
"PROJECT_LINK": project_link,
"PROJECT_ID": str(project_id) if project_id is not None else None,
"TASK_LINK": task_link,
"TASK_ID": str(task_id) if task_id is not None else None,
"PROFILE_LINK": inbox_url,
"SETTINGS_LINK": settings_url,
"CONTENT": format_username_link(content),
Expand Down
8 changes: 7 additions & 1 deletion backend/services/messaging/templates/message_alert_en.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block content %}
{% if values['MESSAGE_TYPE'] in [3, 8, 9] %}
{% if values['MESSAGE_TYPE'] == 3 %}
<p style="font-size: 14px; font-weight: 600"><a style="color: #d73f3f" href="{{values['FROM_USER_LINK']}}">@{{values['FROM_USERNAME']}}</a> mentioned you in a comment on <a style="color: #d73f3f" href="{{values['PROJECT_LINK']}}">Project {{values['PROJECT_ID']}}</a>:</p>
{% endif %}
{% if values['MESSAGE_TYPE'] == 4 %}
Expand All @@ -9,6 +9,12 @@
{% if values['MESSAGE_TYPE'] == 5 %}
<p style="font-size: 14px; font-weight: 600">Task on <a style="color: #d73f3f" href="{{values['PROJECT_LINK']}}">Project {{values['PROJECT_ID']}}</a> needs more mapping:</p>
{% endif %}
{% if values['MESSAGE_TYPE'] == 8 %}
<p style="font-size: 14px; font-weight: 600"><a style="color: #d73f3f" href="{{values['FROM_USER_LINK']}}">@{{values['FROM_USERNAME']}}</a> left a comment on <a style="color: #d73f3f" href="{{values['TASK_LINK']}}">Task {{values['TASK_ID']}}</a> in <a style="color: #d73f3f" href="{{values['PROJECT_LINK']}}">Project {{values['PROJECT_ID']}}</a>:</p>
{% endif %}
{% if values['MESSAGE_TYPE'] == 9 %}
<p style="font-size: 14px; font-weight: 600"><a style="color: #d73f3f" href="{{values['FROM_USER_LINK']}}">@{{values['FROM_USERNAME']}}</a> left a comment on <a style="color: #d73f3f" href="{{values['PROJECT_LINK']}}">Project {{values['PROJECT_ID']}}</a>:</p>
{% endif %}
<p>
{{ values['CONTENT']|safe }}
</p>
Expand Down
25 changes: 13 additions & 12 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"license": "BSD-2-Clause",
"private": false,
"dependencies": {
"@formatjs/intl-locale": "^2.4.8",
"@formatjs/intl-pluralrules": "^4.0.0",
"@formatjs/intl-relativetimeformat": "^8.0.0",
"@formatjs/intl-locale": "^2.4.13",
"@formatjs/intl-pluralrules": "^4.0.3",
"@formatjs/intl-relativetimeformat": "^8.0.3",
"@formatjs/intl-utils": "^3.8.4",
"@formatjs/macro": "^0.2.8",
"@hotosm/id": "^2.19.5",
Expand All @@ -17,19 +17,19 @@
"@mapbox/mapbox-gl-language": "^0.10.1",
"@mapbox/togeojson": "^0.16.0",
"@reach/router": "^1.3.4",
"@sentry/react": "^5.29.0",
"@sentry/tracing": "^5.29.0",
"@sentry/react": "^5.29.2",
"@sentry/tracing": "^5.29.2",
"@turf/area": "^6.0.1",
"@turf/bbox": "^6.0.1",
"@turf/bbox-polygon": "^6.0.1",
"@turf/helpers": "^6.1.4",
"@turf/intersect": "^6.1.3",
"@turf/line-to-polygon": "^6.0.1",
"@turf/transform-scale": "^5.1.5",
"@webscopeio/react-textarea-autocomplete": "^4.7.2",
"axios": "^0.21.0",
"@webscopeio/react-textarea-autocomplete": "^4.7.3",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
"dompurify": "^2.2.4",
"dompurify": "^2.2.6",
"downshift-hooks": "^0.8.1",
"final-form": "^4.20.1",
"fromentries": "^1.3.2",
Expand All @@ -40,16 +40,17 @@
"marked": "^1.2.7",
"node-sass": "^4.14.1",
"osmtogeojson": "^3.0.0-beta.3",
"query-string": "^6.13.7",
"query-string": "^6.13.8",
"react": "^16.14.0",
"react-accessible-accordion": "^3.3.3",
"react-calendar-heatmap": "^1.8.1",
"react-chartjs-2": "^2.11.1",
"react-click-outside": "^3.0.1",
"react-datepicker": "^3.3.0",
"react-dom": "^16.14.0",
"react-dropzone": "^11.2.4",
"react-final-form": "^6.5.2",
"react-intl": "^5.10.6",
"react-intl": "^5.10.12",
"react-meta-elements": "^1.0.0",
"react-placeholder": "^4.0.3",
"react-redux": "^7.2.2",
Expand Down Expand Up @@ -94,12 +95,12 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/jest-dom": "^5.11.8",
"@testing-library/react": "^11.2.2",
"@testing-library/react-hooks": "^3.7.0",
"combine-react-intl-messages": "^4.0.0",
"jest-canvas-mock": "^2.3.0",
"msw": "^0.24.2",
"msw": "^0.25.0",
"prettier": "^2.2.1",
"react-test-renderer": "^16.14.0"
},
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/assets/styles/_accordion.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.accordion_button {
&::before {
display: inline-block;
content: '';
height: 8px;
width: 8px;
margin-right: 12px;
border-bottom: 2px solid currentColor;
border-right: 2px solid currentColor;
transform: rotate(-45deg);
}
}

.accordion_button[aria-expanded='true']::before,
.accordion_button[aria-selected='true']::before {
transform: rotate(45deg);
}

[hidden] {
display: none;
}
.accordion_panel {
animation: fadein 0.35s ease-in;
}

@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
3 changes: 2 additions & 1 deletion frontend/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
@import 'testimonials';
@import 'organizations';
@import 'calendarheatmap';
@import 'datepicker';
@import 'datepicker';
@import 'accordion';
5 changes: 3 additions & 2 deletions frontend/src/components/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export function FormSubmitButton({ children, className, disabledClassName, disab
);
}

export function CustomButton({ onClick, children, className }: Object) {
export function CustomButton({ onClick, children, className, disabled }: Object) {
return (
<button
onClick={onClick}
aria-pressed="false"
focusindex="0"
className={`${className || ''} br1 f5 pointer`}
className={`${className || ''} br1 f5 ${disabled ? 'o-50' : 'pointer'}`}
disabled={disabled}
>
{children}
</button>
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/contributions/myTasksNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ const isActiveButton = (buttonName, contributionQuery) => {
}
};

export const MyTasksNav = props => {
export const MyTasksNav = (props) => {
const [contributionsQuery, setContributionsQuery] = useTaskContributionQueryParams();

const linkCombo = 'link ph3 f6 pv2 ba b--grey-light';
const notAnyFilter = !stringify(contributionsQuery);
return (
/* mb1 mb2-ns (removed for map, but now small gap for more-filters) */
<header className=" w-100 ">
<header className="w-100">
<div className="cf">
<div className="w-75-l w-60 fl">
<h3 className="barlow-condensed f2 ma0 pv3 mt1 v-mid dib ttu pl2 pl0-l">
<h3 className="barlow-condensed blue-dark f2 ma0 pv3 mt1 v-mid dib ttu pl2 pl0-l">
<FormattedMessage {...messages.myTasks} />
</h3>
</div>
Expand All @@ -38,7 +38,7 @@ export const MyTasksNav = props => {
<div className="dib">
<div className="mv2 dib"></div>
<FormattedMessage {...messages.searchProject}>
{msg => {
{(msg) => {
return (
<ProjectSearchBox
className="dib fl mh1"
Expand Down
Loading

0 comments on commit 7ae5c70

Please sign in to comment.