Skip to content

Commit b0e2ced

Browse files
authored
Merge pull request #3962 from akvo/form-submissions-restriction
Form submissions restriction
2 parents 92e5b5b + 2db1552 commit b0e2ced

11 files changed

Lines changed: 475 additions & 6 deletions

File tree

Dashboard/app/css/main.scss

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,50 @@ header.top {
605605
}
606606
}
607607

608+
#submission-warning-banner {
609+
position: fixed;
610+
top: 0;
611+
z-index: 9999;
612+
font-size: 0.9rem;
613+
text-align: center;
614+
color: #212529;
615+
background-color: #ffc107;
616+
}
617+
#submission-warning-banner strong {
618+
font-weight: bold;
619+
}
620+
#submission-warning-banner a {
621+
color: #dc3545;
622+
}
623+
#submission-warning-banner a:hover {
624+
color: #9c1c28;
625+
}
626+
627+
.submission-count {
628+
position: absolute;
629+
top: 7px;
630+
right: 80px;
631+
padding: 2px;
632+
font-size: 14px;
633+
}
634+
635+
.submission-count-badge {
636+
display: inline-block;
637+
padding: 0.35em 0.65em;
638+
font-size: .75em;
639+
font-weight: 700;
640+
line-height: 1;
641+
text-align: center;
642+
white-space: nowrap;
643+
vertical-align: baseline;
644+
//color: #fff;
645+
//background-color: #6c757d;
646+
//background-color: #dc3545!important;
647+
color: #212529;
648+
background-color: #ffc107;
649+
border-radius: 0.4rem!important;
650+
}
651+
608652
.logIn,
609653
.logOut {
610654
position: absolute;

Dashboard/app/js/plugins/flowDashboard.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,28 @@ $(document).ready(function () {
144144
$(".dataTables_paginate").addClass("floats-in");
145145
$(".dataTables_filter label > input").removeAttr("type").attr("type", "search");
146146

147-
147+
if (FLOW.Env.formSubmissionsLimit > 0) {
148+
// Submission count
149+
$.get("/rest/count_form_submissions", function (data) {
150+
const hardLimit = FLOW.Env.formSubmissionsLimit;
151+
const softLimitPercentage = FLOW.Env.formSubmissionsSoftLimitPercentage;
152+
const softLimit = Math.round(hardLimit * (softLimitPercentage / 100));
153+
const value = data.value;
154+
// don't show if there are no submissions yet
155+
if (!value) {
156+
return;
157+
}
158+
// Show banner if greater than softLimit
159+
if (value >= softLimit) {
160+
$(`<div id="submission-warning-banner">You have reached <strong>${Math.round((value/hardLimit) * 100)}% (${value}/${hardLimit})</strong> of form submissions allowed in your FLOW Basic plan. Please contact <a href="mailto:support@akvo.org">support@akvo.org</a> to upgrade your plan and avoid blocking form submissions. <a style="float: right; margin-right: 5px;">[x]</a></div>`).insertBefore("#header>div:first-child");
161+
$('#submission-warning-banner').on('click', 'a', function () {
162+
$('#submission-warning-banner').remove();
163+
});
164+
return;
165+
}
166+
// Show notification badge if less than softLimit
167+
$(`<div class="submission-count">Submissions <span class="submission-count-badge">${value} / ${hardLimit}</span></div>`)
168+
.insertBefore("#header li.logOut");
169+
});
170+
}
148171
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.akvo.flow.domain;
2+
3+
public class FormSubmissionsLimit {
4+
5+
private Integer hardLimit;
6+
private Integer softLimit;
7+
8+
public FormSubmissionsLimit(Integer limit) {
9+
this(limit, 80);
10+
}
11+
12+
public FormSubmissionsLimit(Integer hardLimit, Integer soft_percentage) {
13+
this.hardLimit = hardLimit;
14+
this.softLimit = Math.round(hardLimit * ((float) soft_percentage / 100));
15+
}
16+
17+
public Integer getHardLimit() {
18+
return this.hardLimit;
19+
}
20+
21+
public Integer getSoftLimit() {
22+
return this.softLimit;
23+
}
24+
25+
public boolean isEnabled() {
26+
return this.hardLimit > 0;
27+
}
28+
29+
public Long getPercentage(Integer count) {
30+
return Math.round((((double) count) / hardLimit) * 100);
31+
}
32+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.akvo.flow.domain;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import org.apache.commons.collections.CollectionUtils;
7+
8+
import com.gallatinsystems.user.domain.User;
9+
import com.google.appengine.api.datastore.DatastoreService;
10+
import com.google.appengine.api.datastore.Entity;
11+
import com.google.appengine.api.datastore.FetchOptions;
12+
import com.google.appengine.api.datastore.PreparedQuery;
13+
import com.google.appengine.api.datastore.Query;
14+
import com.google.appengine.api.datastore.Query.FilterOperator;
15+
import com.google.appengine.api.datastore.Query.FilterPredicate;
16+
17+
public class UserFormSubmissionsCounter {
18+
private DatastoreService datastore;
19+
20+
public UserFormSubmissionsCounter(DatastoreService datastore) {
21+
this.datastore = datastore;
22+
}
23+
24+
public Integer countFor(User user) {
25+
List<Long> surveyIds = getSurveyIds(user);
26+
if (CollectionUtils.isEmpty(surveyIds)) {
27+
return 0;
28+
}
29+
30+
Query q = new Query("SurveyInstance").setFilter(new FilterPredicate("surveyId", FilterOperator.IN, surveyIds));
31+
PreparedQuery pq = datastore.prepare(q);
32+
33+
return pq.countEntities(FetchOptions.Builder.withChunkSize(500));
34+
}
35+
36+
private List<Long> getSurveyIds(User user) {
37+
List<Long> ids = new ArrayList<Long>();
38+
Query q = new Query("Survey").setFilter(new FilterPredicate("createUserId", FilterOperator.EQUAL, user.getKey().getId()));
39+
PreparedQuery pq = datastore.prepare(q);
40+
41+
for (Entity s : pq.asIterable(FetchOptions.Builder.withChunkSize(500))) {
42+
ids.add(s.getKey().getId());
43+
}
44+
45+
return ids;
46+
}
47+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.akvo.flow.rest;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import org.akvo.flow.domain.UserFormSubmissionsCounter;
7+
import org.springframework.stereotype.Controller;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RequestMethod;
10+
import org.springframework.web.bind.annotation.ResponseBody;
11+
import org.waterforpeople.mapping.app.web.CurrentUserServlet;
12+
13+
import com.gallatinsystems.user.domain.User;
14+
import com.google.appengine.api.datastore.DatastoreServiceFactory;
15+
16+
@Controller
17+
@RequestMapping("/count_form_submissions")
18+
public class CountFormSubmissionsRestService {
19+
20+
@RequestMapping(method = RequestMethod.GET, value = "")
21+
@ResponseBody
22+
public Map<String, Integer> getMySubmissionsCount() {
23+
Map<String, Integer> response = new HashMap<String, Integer>();
24+
User currentUser = CurrentUserServlet.getCurrentUser();
25+
UserFormSubmissionsCounter counter = new UserFormSubmissionsCounter(DatastoreServiceFactory.getDatastoreService());
26+
27+
Integer value = counter.countFor(currentUser);
28+
29+
response.put("value", value);
30+
31+
return response;
32+
}
33+
}

GAE/src/org/waterforpeople/mapping/app/web/EnvServlet.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public class EnvServlet extends HttpServlet {
5858
public static final String WEBFORM_V2_ENABLED = "enableWebFormV2";
5959
public static final String SELF_ONBOARD_ENABLED = "enableSelfOnboard";
6060
public static final String INDEX_REDIRECT_DISABLED = "disableIndexRedirect";
61+
public static final String FORM_SUBMISSIONS_LIMIT = "formSubmissionsLimit";
62+
public static final String FORM_SUBMISSIONS_SOFT_LIMIT_PERCENTAGE = "formSubmissionsSoftLimitPercentage";
6163

6264

6365
private static final ArrayList<String> properties = new ArrayList<String>();
@@ -87,6 +89,8 @@ public class EnvServlet extends HttpServlet {
8789
properties.add(SHOW_FORM_INSTANCE_API_URL);
8890
properties.add(WEBFORM_V2_ENABLED);
8991
properties.add(SELF_ONBOARD_ENABLED);
92+
properties.add(FORM_SUBMISSIONS_LIMIT);
93+
properties.add(FORM_SUBMISSIONS_SOFT_LIMIT_PERCENTAGE);
9094
}
9195

9296
@Override
@@ -169,6 +173,19 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
169173

170174
props.put(SELF_ONBOARD_ENABLED, Boolean.toString("true".equalsIgnoreCase(props.get(SELF_ONBOARD_ENABLED))));
171175

176+
// Ensure formSubmissionsLimit is a number
177+
try {
178+
props.put(FORM_SUBMISSIONS_LIMIT, Integer.valueOf(props.get(FORM_SUBMISSIONS_LIMIT)).toString());
179+
} catch (NumberFormatException e) {
180+
props.put(FORM_SUBMISSIONS_LIMIT, "0");
181+
}
182+
// Ensure formSubmissionsSoftLimitPercentage is a number
183+
try {
184+
props.put(FORM_SUBMISSIONS_SOFT_LIMIT_PERCENTAGE, Integer.valueOf(props.get(FORM_SUBMISSIONS_SOFT_LIMIT_PERCENTAGE)).toString());
185+
} catch (NumberFormatException e) {
186+
props.put(FORM_SUBMISSIONS_SOFT_LIMIT_PERCENTAGE, "0");
187+
}
188+
172189
if (props.get(CADDISFLY_TESTS_FILE_URL_KEY) == null
173190
|| props.get(CADDISFLY_TESTS_FILE_URL_KEY).isEmpty()) {
174191
props.put("caddisflyTestsFileUrl",

0 commit comments

Comments
 (0)