Skip to content

Commit

Permalink
Merge pull request #872 from Natay/master
Browse files Browse the repository at this point in the history
post diffs computed on edit
  • Loading branch information
ialbert committed Jul 16, 2021
2 parents f2f20ae + 139a813 commit 4bc57f8
Show file tree
Hide file tree
Showing 18 changed files with 260 additions and 61 deletions.
46 changes: 28 additions & 18 deletions biostar/forum/ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
from ratelimit.decorators import ratelimit
from urllib import request as builtin_request
from difflib import Differ
# import requests
from urllib.parse import urlencode
from datetime import datetime, timedelta
Expand All @@ -20,7 +21,7 @@

from biostar.accounts.models import Profile, User
from . import auth, util, forms, tasks, search, views, const, moderate
from .models import Post, Vote, Subscription, delete_post_cache, SharedLink
from .models import Post, Vote, Subscription, delete_post_cache, SharedLink, Diff



Expand Down Expand Up @@ -237,21 +238,6 @@ def get_fields(request, post=None):
return fields


def set_post(post, user, fields):
# Set the fields for this post.
if post.is_toplevel:
post.title = fields.get('title', post.title)
post.type = fields.get('post_type', post.type)
post.tag_val = fields.get('tag_val', post.tag_val)

post.lastedit_user = user
post.lastedit_date = util.now()
post.content = fields.get('content', post.content)
post.save()

return post


@ajax_limited(key=RATELIMIT_KEY, rate=EDIT_RATE)
@ajax_error_wrapper(method="POST", login_required=True)
def ajax_edit(request, uid):
Expand Down Expand Up @@ -279,7 +265,7 @@ def ajax_edit(request, uid):
form = forms.PostShortForm(post=post, user=user, data=fields)

if form.is_valid():
post = set_post(post, user, form.cleaned_data)
form.edit()
return ajax_success(msg='Edited post', redirect=post.get_absolute_url())
else:
msg = [field.errors for field in form if field.errors]
Expand Down Expand Up @@ -321,7 +307,7 @@ def ajax_comment_create(request):

if form.is_valid():
# Create the comment.
post = Post.objects.create(type=Post.COMMENT, content=content, author=user, parent=parent)
post = form.save()
return ajax_success(msg='Created post', redirect=post.get_absolute_url())
else:
msg = [field.errors for field in form if field.errors]
Expand Down Expand Up @@ -470,6 +456,30 @@ def email_disable(request, uid):
return ajax_success(msg='Disabled messages')


@ajax_error_wrapper(method="POST", login_required=True)
def view_diff(request, uid):
"""
View most recent diff to a post.
"""

# View most recent diff made to a post
post = Post.objects.filter(uid=uid).first()

diffs = Diff.objects.filter(post=post).order_by('-pk')

# Post has no recorded changes,
if not diffs.exists():
return ajax_success(has_changes=False, msg='Post has no recorded changes')

# Change new line chars to break line tags.
context = dict(diffs=diffs)
tmpl = loader.get_template(template_name='diff.html')
tmpl = tmpl.render(context)

# Return newly created diff
return ajax_success(has_changes=True, msg='Showing changes', diff=tmpl)


def similar_posts(request, uid):
"""
Return a feed populated with posts similar to the one in the request.
Expand Down
72 changes: 62 additions & 10 deletions biostar/forum/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import re
import urllib.parse as urlparse
from datetime import timedelta

from difflib import Differ, SequenceMatcher, HtmlDiff, unified_diff
import bs4
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.core.cache import cache
Expand All @@ -21,7 +22,7 @@
from biostar.utils.helpers import get_ip
from . import util, awards
from .const import *
from .models import Post, Vote, Subscription, Badge, delete_post_cache, Log, SharedLink
from .models import Post, Vote, Subscription, Badge, delete_post_cache, Log, SharedLink, Diff

User = get_user_model()

Expand Down Expand Up @@ -67,7 +68,8 @@ def delete_cache(prefix, user):

import datetime

ICONS = [ "monsterid", "robohash", "wavatar", "retro"]
ICONS = ["monsterid", "robohash", "wavatar", "retro"]


def gravatar_url(email, style='mp', size=80, force=None):
global ICONS
Expand Down Expand Up @@ -208,18 +210,19 @@ def create_post_from_json(**json_data):
return


def create_post(author, title, content, request, root=None, parent=None, ptype=Post.QUESTION, tag_val="", nodups=True):
def create_post(author, title, content, request=None, root=None, parent=None, ptype=Post.QUESTION, tag_val="",
nodups=True):
# Check if a post with this exact content already exists.
post = Post.objects.filter(content=content, author=author).order_by('-creation_date').first()

# How many seconds since the last post should we disallow duplicates.
time_frame = 60
if nodups and post:
# Check to see if this post was made within given timeframe
delta_secs = (util.now() - post.creation_date).seconds
if delta_secs < time_frame:
frame = 60
delta = (util.now() - post.creation_date).seconds if post else frame

if nodups and delta < frame:
if request:
messages.warning(request, "Post with this content was created recently.")
return post
return post

post = Post.objects.create(title=title, content=content, root=root, parent=parent,
type=ptype, tag_val=tag_val, author=author)
Expand All @@ -228,6 +231,55 @@ def create_post(author, title, content, request, root=None, parent=None, ptype=P
return post


def diff_ratio(text1, text2):

# Do not match on spaces
s = SequenceMatcher(lambda char: re.match(r'\w+', char), text1, text2)
return round(s.ratio(), 5)


def compute_diff(text, post, user):
"""
Compute and return Diff object for diff between text and post.content
"""

# Skip on post creation
if not post:
return

ratio = diff_ratio(text1=text, text2=post.content)

# Skip no changes detected
if ratio == 1:
return

# Compute diff between text and post.
content = post.content.splitlines()
text = text.splitlines()

diff = unified_diff(content, text)
diff = [f"{line}\n" if not line.endswith('\n') else line for line in diff]
diff = ''.join(diff)

# See if a diff has been made by this user in the past 10 minutes
dobj = Diff.objects.filter(post=post, author=post.author).first()

# 10 minute time frame between
frame = 6 * 100
delta = (util.now() - dobj.created).seconds if dobj else frame

# Create diff object within time frame or the person editing is a mod.
if delta >= frame or user != post.author:
# Create diff object for this user.
dobj = Diff.objects.create(diff=diff, post=post, author=user)

# Only log when anyone but the author commits changes.
if user != post.author:
db_logger(user=user, action=Log.EDIT, text=f'edited post', target=post.author, post=post)

return dobj


def merge_profiles(main, alias):
"""
Merge alias profile into main
Expand Down
33 changes: 26 additions & 7 deletions biostar/forum/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from snowpenguin.django.recaptcha2.widgets import ReCaptchaWidget
from biostar.accounts.models import User
from .models import Post, SharedLink
from biostar.forum import models, auth
from biostar.forum import models, auth, util

from .const import *

Expand Down Expand Up @@ -146,10 +146,16 @@ def edit(self):
if self.user != self.post.author and not self.user.profile.is_moderator:
raise forms.ValidationError("Only the author or a moderator can edit a post.")
data = self.cleaned_data

self.post.title = data.get('title')
self.post.content = data.get("content")
content = data.get('content', self.post.content)
# Calculate diff and save to db
auth.compute_diff(text=content, post=self.post, user=self.user)
self.post.content = content

self.post.type = data.get('post_type')
self.post.tag_val = data.get('tag_val')
self.post.lastedit_date = util.now()
self.post.lastedit_user = self.user
self.post.save()
return self.post
Expand Down Expand Up @@ -187,13 +193,12 @@ def clean_content(self):

class PostShortForm(forms.Form):
MIN_LEN, MAX_LEN = 10, 10000
parent_uid = forms.CharField(widget=forms.HiddenInput(), min_length=2, max_length=32, required=False)
content = forms.CharField(widget=forms.Textarea,
min_length=MIN_LEN, max_length=MAX_LEN, strip=False)
content = forms.CharField(widget=forms.Textarea, min_length=MIN_LEN, max_length=MAX_LEN, strip=False)

def __init__(self, user=None, post=None, *args, **kwargs):
def __init__(self, post, user=None, request=None, ptype=Post.COMMENT, *args, **kwargs):
self.user = user
self.post = post
self.ptype = ptype
super().__init__(*args, **kwargs)
self.fields['content'].strip = False

Expand All @@ -207,9 +212,23 @@ def clean(self):
raise forms.ValidationError("You need to be logged in.")
return cleaned_data

def edit(self):
# Set the fields for this post.
self.post.lastedit_user = self.user
self.post.lastedit_date = util.now()
content = self.cleaned_data.get('content', self.post.content)
auth.compute_diff(text=content, post=self.post, user=self.user)
self.post.content = content
self.post.save()

def save(self):
content = self.cleaned_data.get('content', self.post.content)
post = auth.create_post(parent=self.post, author=self.user, content=content, ptype=self.ptype,
root=self.post.root, title=self.post.title)
return post

class MergeProfiles(forms.Form):

class MergeProfiles(forms.Form):
main = forms.CharField(label='Main user email', max_length=100, required=True)
alias = forms.CharField(label='Alias email to merge to main', max_length=100, required=True)

Expand Down
3 changes: 2 additions & 1 deletion biostar/forum/herald.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def herald_publisher(request, limit=20, nmin=1):
# Reset status on published links.
# SharedLink.objects.filter(status=SharedLink.PUBLISHED).update(status=SharedLink.ACCEPTED)

heralds = SharedLink.objects.filter(status=SharedLink.ACCEPTED).order_by('-creation_date')[:limit]
heralds = SharedLink.objects.filter(status=SharedLink.ACCEPTED).order_by('-pk')[:limit]
count = heralds.count()

if count < nmin:
Expand Down Expand Up @@ -177,6 +177,7 @@ def herald_list(request):
# Add the Link attribute.
link = form.cleaned_data['url']
text = form.cleaned_data['text']

# Create the herald_list objects.
herald = SharedLink.objects.create(author=user, text=text, url=link)
messages.success(request, 'Submitted for review.')
Expand Down
12 changes: 7 additions & 5 deletions biostar/forum/management/commands/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from biostar import VERSION
from django.core.management.base import BaseCommand
from biostar.forum import models, util, tasks
from biostar.forum.models import Post
from biostar.forum.models import Post, Diff
from django.conf import settings
from biostar.accounts.models import User
from biostar.utils.decorators import timeit
Expand All @@ -17,8 +17,8 @@

BACKUP_DIR = os.path.join(settings.BASE_DIR, 'export', 'backup')

BUMP, UNBUMP, AWARD = 'bump', 'unbump', 'award'
CHOICES = [BUMP, UNBUMP, AWARD]
CHOICES = ['bump', 'unbump', 'award']
BUMP, UNBUMP, AWARD = CHOICES


def bump(uids, **kwargs):
Expand Down Expand Up @@ -83,8 +83,10 @@ class Command(BaseCommand):

def add_arguments(self, parser):
parser.add_argument('--uids', '-u', type=str, required=False, default='', help='List of uids')
parser.add_argument('--action', '-a', type=str, required=True, choices=CHOICES, default='',help='Action to take.')
parser.add_argument('--limit', dest='limit', type=int, default=100, help='Limit how many users/posts to process.'),
parser.add_argument('--action', '-a', type=str, required=True, choices=CHOICES, default='',
help='Action to take.')
parser.add_argument('--limit', dest='limit', type=int, default=100,
help='Limit how many users/posts to process.'),

def handle(self, *args, **options):
action = options['action']
Expand Down
26 changes: 26 additions & 0 deletions biostar/forum/migrations/0021_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2 on 2021-07-11 19:58

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('forum', '0020_sharedlink_status'),
]

operations = [
migrations.CreateModel(
name='Diff',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('diff', models.TextField(default='')),
('created', models.DateTimeField(auto_now_add=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='forum.post')),
],
),
]
28 changes: 28 additions & 0 deletions biostar/forum/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,34 @@ def save(self, *args, **kwargs):
def uid(self):
return self.pk


class Diff(models.Model):

# Initial content state
diff = models.TextField(default='')

# Date this change was made.
created = models.DateTimeField(auto_now_add=True)

# Post this diff belongs to
post = models.ForeignKey(Post, on_delete=models.CASCADE)

# Person who created the diff
author = models.ForeignKey(User, on_delete=models.CASCADE)

def save(self, *args, **kwargs):

self.created = self.created or util.now()

super(Diff, self).save(*args, **kwargs)

@property
def breakline(self):
diff = self.diff
diff = diff.replace('\n', '<br>')
return diff


class Log(models.Model):
"""
Represents moderation actions
Expand Down

0 comments on commit 4bc57f8

Please sign in to comment.