Skip to content

Commit

Permalink
Neil/lost updates (#341)
Browse files Browse the repository at this point in the history
* Created version number field; check version number field on each update request

* Handled delete case + refactor
  • Loading branch information
DumboOctopus authored Oct 19, 2020
1 parent acfb6ad commit 68d6d8f
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 83 deletions.
6 changes: 5 additions & 1 deletion meow/frontend/src/actions/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const savePost = (postId, postData) => dispatch => {
},
err => {
if (err.response.status === 403) {
console.log("Logging off because of 403 from saving posts");
// console.log("Logging off because of 403 from saving posts");
dispatch({
type: "LOGOUT"
});
Expand All @@ -41,6 +41,10 @@ export const savePost = (postId, postData) => dispatch => {
if (data["story_url"] !== undefined) {
description = "Invalid URL";
}
if (data["error"] !== undefined) {
description = data["error"];
}
console.log(data);
alertError("Saving failed :(", description)(dispatch);
dispatch({
type: "NETWORK_ERROR",
Expand Down
1 change: 1 addition & 0 deletions meow/frontend/src/components/AddPost/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const AddPost = () => {
pub_ready_copy: false,
pub_ready_online: false,
section: null,
version_number: 0,
pub_date: new Intl.DateTimeFormat("en-GB", dateOpts)
.format(date)
.split("/")
Expand Down
15 changes: 9 additions & 6 deletions meow/frontend/src/components/EditPost/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class EditPost extends React.Component {
post_instagram: this.state.post_instagram,
pub_ready_copy: this.state.pub_ready_copy,
pub_ready_online: this.state.pub_ready_online,
version_number: this.state.version_number,
tags: this.state.tags
? this.state.tags.map(x => {
return x.text;
Expand All @@ -138,12 +139,14 @@ class EditPost extends React.Component {
deletePost = () => {
const { postId } = this.props.match.params;

this.props.savePost(postId, { is_active: false }).then(data => {
if (data) {
this.props.history.push("/");
} else {
}
});
this.props
.savePost(postId, { is_active: false, version_number: this.state.version_number })
.then(data => {
if (data) {
this.props.history.push("/");
} else {
}
});
};

sendNow = () => {
Expand Down
18 changes: 18 additions & 0 deletions meow/scheduler/migrations/0018_smpost_version_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.6 on 2020-10-15 22:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('scheduler', '0017_smpost_post_instagram'),
]

operations = [
migrations.AddField(
model_name='smpost',
name='version_number',
field=models.IntegerField(default=0),
),
]
1 change: 1 addition & 0 deletions meow/scheduler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class SMPost(models.Model):
default=True, help_text="If false, consider mock-deleted.")

tags = models.ManyToManyField(SMPostTag)
version_number = models.IntegerField(default=0)

def __str__(self):
return "" if self.slug is None else self.slug
Expand Down
130 changes: 82 additions & 48 deletions meow/scheduler/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
# Oauth stuff
from requests_oauthlib import OAuth2Session
from requests_oauthlib.compliance_fixes import facebook_compliance_fix
from django.db.models import F
from django.db import transaction



Expand Down Expand Up @@ -110,6 +112,12 @@ def get_object(self, post_id):
except SMPost.DoesNotExist:
raise Http404

def get_object_with_lock(self, post_id):
try:
return SMPost.objects.select_for_update().get(id=post_id)
except SMPost.DoesNotExist:
raise Http404

def get(self, request, post_id, format=None):
if not request.user.is_authenticated:
return Response("Must be logged in", status=403)
Expand All @@ -118,60 +126,86 @@ def get(self, request, post_id, format=None):
serializer = SMPostSerializer(post)
return Response(serializer.data)

def put(self, request, post_id, format=None):
if not request.user.is_authenticated:
return Response("Must be logged in", status=403)

post = self.get_object(post_id)

serializer = SMPostSerializer(post, data=request.data)
if serializer.is_valid():
b_should_update_copy_user = False
update_copy_user_to = None
b_should_update_online_user = False
update_online_user_to = None

#print(type(request.data["pub_ready_copy"]));

if "pub_ready_copy" in request.data and post.pub_ready_copy != request.data["pub_ready_copy"]:
# it means that the sender of this request tried to change it
# we have to check if they have copy permissions
#if request.user.group
if request.user.groups.filter(name="Copy").count() <= 0: # user is not part of copy group
# TODO: what data should the response send back
return Response({"error":"Permission denied"}, status=status.HTTP_400_BAD_REQUEST)
def user_action_history_update(self, request, post):
"""
Returns a tuple
success, object
it will be unsuccessful only if the user does nott have permission
"""
b_should_update_copy_user = False
update_copy_user_to = None
b_should_update_online_user = False
update_online_user_to = None


if "pub_ready_copy" in request.data and post.pub_ready_copy != request.data["pub_ready_copy"]:
# it means that the sender of this request tried to change it
# we have to check if they have copy permissions
if request.user.groups.filter(name="Copy").count() <= 0: # user is not part of copy group
# TODO: what data should the response send back
return False, None
else:
b_should_update_copy_user = True
if request.data["pub_ready_copy"]:
update_copy_user_to = request.user
else:
b_should_update_copy_user = True
if request.data["pub_ready_copy"]:
update_copy_user_to = request.user
else:
update_copy_user_to = None # means that the user marked it as not copy edited so clear the copy edited user
if "pub_ready_online" in request.data and post.pub_ready_online != request.data["pub_ready_online"]:

if request.user.groups.filter(name="Online").count() <= 0: # user is not part of group
return Response({"error":"Permission denied"}, status=status.HTTP_400_BAD_REQUEST)
update_copy_user_to = None # means that the user marked it as not copy edited so clear the copy edited user
if "pub_ready_online" in request.data and post.pub_ready_online != request.data["pub_ready_online"]:

if request.user.groups.filter(name="Online").count() <= 0: # user is not part of group
return False, None
else:
b_should_update_online_user = True
if request.data["pub_ready_online"]:
update_online_user_to = request.user
else:
b_should_update_online_user = True
if request.data["pub_ready_online"]:
update_online_user_to = request.user
else:
update_online_user_to = None

# will be passed into serializer.save()
serializer_keyword_args = {
"last_edit_user": request.user
}
update_online_user_to = None

# will be passed into serializer.save()
serializer_keyword_args = {
"last_edit_user": request.user
}

# if the user updated the copy edited or online approved status,
# record them as the copy_user or online_user
if b_should_update_copy_user:
serializer_keyword_args["pub_ready_copy_user"] = update_copy_user_to
# if the user updated the copy edited or online approved status,
# record them as the copy_user or online_user
if b_should_update_copy_user:
serializer_keyword_args["pub_ready_copy_user"] = update_copy_user_to

if b_should_update_online_user:
serializer_keyword_args["pub_ready_online_user"] = update_online_user_to
if b_should_update_online_user:
serializer_keyword_args["pub_ready_online_user"] = update_online_user_to

post = serializer.save(**serializer_keyword_args)
return True, serializer_keyword_args


def put(self, request, post_id, format=None):
if not request.user.is_authenticated:
return Response("Must be logged in", status=403)


with transaction.atomic():
# lock the row. This lock will be released at the end of this transaction
# https://docs.djangoproject.com/en/3.1/ref/models/querysets/#select-for-update
post = self.get_object_with_lock(post_id)

serializer = SMPostSerializer(post, data=request.data)
if serializer.is_valid():
success, serializer_keyword_args = self.user_action_history_update(request, post)
if not success:
return Response({"error": "Permission denied"})
# version number is incremented each time someone updates
# a post. If someone saves while another person is editing,
# the editing person will not see their changes. When
# the editing person saves, the first person's updates will be lost
# to prevent this and other confusion, we have this check
if request.data["version_number"] < post.version_number:
return Response({"error":"Someone updated this meow before you saved! Please open this meow in a new tab and reapply your changes."}, status=status.HTTP_400_BAD_REQUEST)

post = serializer.save(**serializer_keyword_args)
# increment version number
post.version_number += 1
post.save()
# at the end of the transaction, the lock will automatically be released
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Expand Down
38 changes: 10 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 68d6d8f

Please sign in to comment.