mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 00:43:53 +00:00
Add file shared link, and modify url viewing file
This commit is contained in:
10
forms.py
10
forms.py
@@ -32,3 +32,13 @@ class AddUserForm(forms.Form):
|
|||||||
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
|
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
|
||||||
raise forms.ValidationError(_("The two password fields didn't match."))
|
raise forms.ValidationError(_("The two password fields didn't match."))
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
class FileLinkShareForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Form for sharing file shared link to emails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
email = forms.CharField(max_length=512)
|
||||||
|
file_shared_link = forms.CharField(max_length=40)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -466,6 +466,7 @@ table img {
|
|||||||
margin-top:8px;
|
margin-top:8px;
|
||||||
}
|
}
|
||||||
/*repo-share-form*/
|
/*repo-share-form*/
|
||||||
|
#email,
|
||||||
#email_or_group,
|
#email_or_group,
|
||||||
#share-link,
|
#share-link,
|
||||||
#added-member-name {
|
#added-member-name {
|
||||||
@@ -653,3 +654,4 @@ table img {
|
|||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
max-height: 550px;
|
max-height: 550px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,21 @@
|
|||||||
|
import datetime
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class AnonymousShare(models.Model):
|
class AnonymousShare(models.Model):
|
||||||
|
"""
|
||||||
|
Model used for sharing repo to unregistered email.
|
||||||
|
"""
|
||||||
repo_owner = models.EmailField(max_length=255)
|
repo_owner = models.EmailField(max_length=255)
|
||||||
repo_id = models.CharField(max_length=36)
|
repo_id = models.CharField(max_length=36)
|
||||||
anonymous_email = models.EmailField(max_length=255)
|
anonymous_email = models.EmailField(max_length=255)
|
||||||
token = models.CharField(max_length=25, unique=True)
|
token = models.CharField(max_length=25, unique=True)
|
||||||
|
|
||||||
|
class FileShare(models.Model):
|
||||||
|
"""
|
||||||
|
Model used for file share link.
|
||||||
|
"""
|
||||||
|
username = models.EmailField(max_length=255)
|
||||||
|
repo_id = models.CharField(max_length=36, db_index=True)
|
||||||
|
path = models.TextField()
|
||||||
|
token = models.CharField(max_length=10, unique=True)
|
||||||
|
ctime = models.DateTimeField(default=datetime.datetime.now)
|
||||||
|
@@ -153,9 +153,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="icon-container"><img src="{{ MEDIA_URL }}img/{{ dirent.obj_name|file_icon_filter }}" alt="文件" /></td>
|
<td class="icon-container"><img src="{{ MEDIA_URL }}img/{{ dirent.obj_name|file_icon_filter }}" alt="文件" /></td>
|
||||||
{% if view_history %}
|
{% if view_history %}
|
||||||
<td><a class="op" href="{{ SITE_ROOT }}repo/{{ repo.props.id }}/view/{{ dirent.props.obj_id }}/?commit_id={{ current_commit.id }}&file_name={{ dirent.props.obj_name }}&p={{ path|urlencode }}{{ dirent.obj_name|urlencode }}">{{ dirent.props.obj_name }}</a></td>
|
<td><a class="op" href="{{ SITE_ROOT }}repo/{{ repo.props.id }}/files/?obj_id={{ dirent.props.obj_id }}&commit_id={{ current_commit.id }}&p={{ path|urlencode }}{{ dirent.obj_name|urlencode }}">{{ dirent.props.obj_name }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><a class="op" href="{{ SITE_ROOT }}repo/{{ repo.props.id }}/view/{{ dirent.props.obj_id }}/?file_name={{ dirent.props.obj_name }}&p={{ path|urlencode }}{{ dirent.obj_name|urlencode }}">{{ dirent.props.obj_name }}</a></td>
|
<td><a class="op" href="{{ SITE_ROOT }}repo/{{ repo.props.id }}/files/?p={{ path|urlencode }}{{ dirent.obj_name|urlencode }}">{{ dirent.props.obj_name }}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td>{{ dirent.file_size|filesizeformat }}</td>
|
<td>{{ dirent.file_size|filesizeformat }}</td>
|
||||||
|
@@ -25,6 +25,17 @@
|
|||||||
<p><a href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=view" target="_blank">查看原始文件</a></p>
|
<p><a href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=view" target="_blank">查看原始文件</a></p>
|
||||||
<p><a href="{{ SITE_ROOT }}repo/file_revisions/{{ repo.id }}/?p={{ path|urlencode }}">查看所有历史版本</a></p>
|
<p><a href="{{ SITE_ROOT }}repo/file_revisions/{{ repo.id }}/?p={{ path|urlencode }}">查看所有历史版本</a></p>
|
||||||
<p><a href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=download" target="_blank">下载文件</a></p>
|
<p><a href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=download" target="_blank">下载文件</a></p>
|
||||||
|
|
||||||
|
{% if not view_history %}
|
||||||
|
<h3>共享</h3>
|
||||||
|
<p class="shared-link">
|
||||||
|
<span class="hide">链接:{{ file_shared_link }}</span>
|
||||||
|
<a href="#" data="{{ SITE_ROOT }}sharedlink/get/?repo_id={{ repo.id }}&p={{ path|urlencode }}&file_name={{ file_name }}" class="get-shared-link">获取分享链接</a></p>
|
||||||
|
<div class="link-op hide">
|
||||||
|
<a href="#" class="send-shared-link">发送</a>
|
||||||
|
<a href="#" data="{{ SITE_ROOT }}sharedlink/remove/?t={{ fileshare.token }}" class="remove-shared-link">删除</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main fleft">
|
<div class="main fleft">
|
||||||
@@ -44,17 +55,53 @@
|
|||||||
</p>
|
</p>
|
||||||
<pre id="file-content">正在读取文件内容...</pre>
|
<pre id="file-content">正在读取文件内容...</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form id="link-send-form" action="" method="post" name="link-send-form" class="hide">
|
||||||
|
<label>邮箱:</label><br />
|
||||||
|
<textarea id="email" name="email"></textarea><br />
|
||||||
|
<input id="file_shared_link" type="hidden" name="file_shared_link" value="{{ file_shared_link }}" />
|
||||||
|
<p class="error hide">输入不能为空。</p>
|
||||||
|
<p class="success hide"></p>
|
||||||
|
<p class="sending hide">发送中...</p>
|
||||||
|
<input type="submit" value="提交" id="share-submit-btn" />
|
||||||
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_script %}
|
{% block extra_script %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(window).load(function() {
|
function showLink() {
|
||||||
|
$('.shared-link a').addClass('hide');
|
||||||
|
$('.shared-link span').removeClass('hide');
|
||||||
|
$('.link-op').removeClass('hide');
|
||||||
|
}
|
||||||
|
function hideLink() {
|
||||||
|
$('.shared-link span').addClass('hide');
|
||||||
|
$('.link-op').addClass('hide');
|
||||||
|
$('.shared-link a').removeClass('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).load(function() {
|
||||||
var can_preview = "{{ can_preview }}";
|
var can_preview = "{{ can_preview }}";
|
||||||
|
var view_history = "{{ view_history }}";
|
||||||
|
var url = "";
|
||||||
|
if (view_history == 'True') {
|
||||||
|
url = "{{ SITE_ROOT }}repo/{{ repo.id }}/file/get/?obj_id={{ obj_id }}&p={{ path }}&t={{ token }}&u={{ request.user.username }}";
|
||||||
|
} else {
|
||||||
|
url = "{{ SITE_ROOT }}repo/{{ repo.id }}/file/get/?p={{ path }}&t={{ token }}&u={{ request.user.username }}";
|
||||||
|
}
|
||||||
|
|
||||||
var filetype = "{{ filetype }}";
|
var filetype = "{{ filetype }}";
|
||||||
|
var t = "{{ fileshare.token }}";
|
||||||
|
if (t) {
|
||||||
|
showLink();
|
||||||
|
} else {
|
||||||
|
hideLink();
|
||||||
|
}
|
||||||
|
|
||||||
if (can_preview == 'True' && filetype == 'Document') {
|
if (can_preview == 'True' && filetype == 'Document') {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '{{ SITE_ROOT }}repo/{{ repo.id }}/view/{{ obj_id }}/?file_name={{ file_name }}&t={{ token }}',
|
url: url,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
cache: false,
|
cache: false,
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json; charset=utf-8',
|
||||||
@@ -69,12 +116,119 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
} else if (can_preview == 'True' && filetype == 'Image') {
|
} else if (can_preview == 'True' && filetype == 'Image') {
|
||||||
$('#file-content').replaceWith('<img class="img-preview" src="{{ raw_path }}"></img>');
|
$('#file-content').replaceWith('<img class="img-preview" src="{{ raw_path }}"></img>');
|
||||||
} else {
|
} else {
|
||||||
$('#file-content').html('无法识别该文件格式,<a class="op" href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=download">下载文件</a>。');
|
$('#file-content').html('无法识别该文件格式,<a class="op" href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=download">下载文件</a>。');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.get-shared-link').click(function() {
|
||||||
|
var url = $(this).attr('data');
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
cache: false,
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
var t = data[0]['token'];
|
||||||
|
var shared_link = '{{ protocol }}://{{ domain }}{{ SITE_ROOT }}f/' + t + '/';
|
||||||
|
$('.shared-link span').html('链接:' + shared_link);
|
||||||
|
$('.remove-shared-link').attr('data', '{{ SITE_ROOT }}sharedlink/remove/?t='+t);
|
||||||
|
showLink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, ajaxOptions, thrownError) {
|
||||||
|
var jsonVal = jQuery.parseJSON(xhr.responseText);
|
||||||
|
$('.get-shared-link').html(jsonVal[0]['error']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.remove-shared-link').click(function() {
|
||||||
|
var url = $(this).attr('data');
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
cache: false,
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
success: function(data) {
|
||||||
|
hideLink();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.send-shared-link').click(function() {
|
||||||
|
$("#link-send-form").modal({appendTo: "#main"});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#link-send-form").submit(function(event) {
|
||||||
|
// clear error and sucess msg
|
||||||
|
$('.error').html("").addClass('hide');
|
||||||
|
$('.success').html("").addClass('hide');
|
||||||
|
$('.sending').removeClass('hide');
|
||||||
|
$('#simplemodal-container').css('height', $('#link-send-form').height());
|
||||||
|
|
||||||
|
var form = $(this),
|
||||||
|
file_shared_link = form.children('input[name="file_shared_link"]').val(),
|
||||||
|
email = form.children('textarea[name="email"]').val();
|
||||||
|
|
||||||
|
if (email && email.length <= 512) {
|
||||||
|
// prepare django csrf token
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
function getCookie(name) {
|
||||||
|
var cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie != '') {
|
||||||
|
var cookies = document.cookie.split(';');
|
||||||
|
for (var i = 0; i < cookies.length; i++) {
|
||||||
|
var cookie = jQuery.trim(cookies[i]);
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
|
||||||
|
// Only send the token to relative URLs i.e. locally.
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "{{ SITE_ROOT }}sharedlink/send/",
|
||||||
|
dataType: 'json',
|
||||||
|
cache: false,
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
data: "file_shared_link="+file_shared_link+"&email="+email,
|
||||||
|
success: function(data) {
|
||||||
|
form.children('.error').addClass('hide');
|
||||||
|
form.children('.sending').addClass('hide');
|
||||||
|
form.children('.success').html(data[0]['msg']).removeClass('hide');
|
||||||
|
$('#simplemodal-container').css('height', $('#link-send-form').height());
|
||||||
|
},
|
||||||
|
error: function(xhr, ajaxOptions, thrownError) {
|
||||||
|
var jsonVal = jQuery.parseJSON(xhr.responseText);
|
||||||
|
form.children('.success').addClass('hide');
|
||||||
|
form.children('.sending').addClass('hide');
|
||||||
|
form.children('.error').html(jsonVal[0]['error']).removeClass('hide');
|
||||||
|
$('#simplemodal-container').css('height', $('#link-send-form').height());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.children('.success').html("").addClass('hide');
|
||||||
|
form.children('.sending').addClass('hide');
|
||||||
|
form.children('.error').removeClass('hide');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
12
templates/shared_link_email.html
Normal file
12
templates/shared_link_email.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{% autoescape off %}
|
||||||
|
亲爱的 {{ to_email }}:
|
||||||
|
|
||||||
|
{{ email }} 在SeaCloud上共享了一个文件给你,请点击以下链接查看:
|
||||||
|
|
||||||
|
{{ file_shared_link }}
|
||||||
|
|
||||||
|
感谢使用我们的网站!
|
||||||
|
|
||||||
|
Seafile团队
|
||||||
|
|
||||||
|
{% endautoescape %}
|
54
templates/view_shared_file.html
Normal file
54
templates/view_shared_file.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{% extends "myhome_base.html" %}
|
||||||
|
{% load seahub_tags %}
|
||||||
|
|
||||||
|
{% block main_panel %}
|
||||||
|
<h2 class="subject">
|
||||||
|
{{ file_name }}<br />
|
||||||
|
<span class="latest-commit-time-author">共享者:{{ username }}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="side fright">
|
||||||
|
<h3>操作</h3>
|
||||||
|
<p><a href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=view" target="_blank">查看原始文件</a></p>
|
||||||
|
<p><a href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=download" target="_blank">下载文件</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main fleft">
|
||||||
|
<pre id="file-content">正在读取文件内容...</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_script %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(window).load(function() {
|
||||||
|
var can_preview = "{{ can_preview }}";
|
||||||
|
var url = "{{ SITE_ROOT }}repo/{{ repo.id }}/file/get/?t={{ access_token }}&p={{ path }}&u={{ username }}";
|
||||||
|
var filetype = "{{ filetype }}";
|
||||||
|
|
||||||
|
if (can_preview == 'True' && filetype == 'Document') {
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
cache: false,
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
$('#file-content').html(data[0]['content']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, ajaxOptions, thrownError) {
|
||||||
|
var jsonVal = jQuery.parseJSON(xhr.responseText);
|
||||||
|
$('#file-content').html(jsonVal[0]['error']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else if (can_preview == 'True' && filetype == 'Image') {
|
||||||
|
$('#file-content').replaceWith('<img class="img-preview" src="{{ raw_path }}"></img>');
|
||||||
|
} else {
|
||||||
|
$('#file-content').html('无法识别该文件格式,<a class="op" href="{{ SITE_ROOT }}repo/{{ repo.id }}/{{ obj_id }}/?file_name={{ file_name }}&op=download">下载文件</a>。');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
18
urls.py
18
urls.py
@@ -6,13 +6,14 @@ from seahub.views import root, peers, myhome, \
|
|||||||
repo, repo_history, modify_token, remove_repo, sys_seafadmin, sys_useradmin, \
|
repo, repo_history, modify_token, remove_repo, sys_seafadmin, sys_useradmin, \
|
||||||
org_seafadmin, org_useradmin, org_group_admin, org_remove, \
|
org_seafadmin, org_useradmin, org_group_admin, org_remove, \
|
||||||
activate_user, user_add, user_remove, sys_group_admin, sys_org_admin, \
|
activate_user, user_add, user_remove, sys_group_admin, sys_org_admin, \
|
||||||
ownerhome, repo_history_revert, \
|
ownerhome, repo_history_revert, repo_file_get, \
|
||||||
user_info, repo_set_access_property, repo_access_file, \
|
user_info, repo_set_access_property, repo_access_file, \
|
||||||
repo_remove_share, repo_download, org_info, repo_view_file, \
|
repo_remove_share, repo_download, org_info, repo_view_file, \
|
||||||
seafile_access_check, back_local, repo_history_changes, \
|
seafile_access_check, back_local, repo_history_changes, \
|
||||||
repo_upload_file, file_upload_progress, file_upload_progress_page, get_subdir, file_move, \
|
repo_upload_file, file_upload_progress, file_upload_progress_page, \
|
||||||
repo_new_dir, repo_rename_file, validate_filename, \
|
get_subdir, file_move, repo_new_dir, repo_rename_file, validate_filename, \
|
||||||
repo_create, repo_update_file, file_revisions
|
repo_create, repo_update_file, file_revisions, \
|
||||||
|
get_shared_link, view_shared_file, remove_shared_link, send_shared_link
|
||||||
from seahub.notifications.views import notification_list
|
from seahub.notifications.views import notification_list
|
||||||
from seahub.share.views import share_admin
|
from seahub.share.views import share_admin
|
||||||
from seahub.group.views import group_list
|
from seahub.group.views import group_list
|
||||||
@@ -42,7 +43,11 @@ urlpatterns = patterns('',
|
|||||||
(r'^share/', include('share.urls')),
|
(r'^share/', include('share.urls')),
|
||||||
url(r'^shareadmin/$', share_admin, name='share_admin'),
|
url(r'^shareadmin/$', share_admin, name='share_admin'),
|
||||||
(r'^shareadmin/removeshare/$', repo_remove_share),
|
(r'^shareadmin/removeshare/$', repo_remove_share),
|
||||||
|
(r'^sharedlink/get/$', get_shared_link),
|
||||||
|
(r'^sharedlink/remove/$', remove_shared_link),
|
||||||
|
(r'^sharedlink/send/$', send_shared_link),
|
||||||
|
(r'^f/(?P<token>[^/]+)/$', view_shared_file),
|
||||||
|
|
||||||
(r'^file_upload_progress/$', file_upload_progress),
|
(r'^file_upload_progress/$', file_upload_progress),
|
||||||
(r'^file_upload_progress_page/$', file_upload_progress_page),
|
(r'^file_upload_progress_page/$', file_upload_progress_page),
|
||||||
(r'^repo/new_dir/$', repo_new_dir),
|
(r'^repo/new_dir/$', repo_new_dir),
|
||||||
@@ -61,7 +66,8 @@ urlpatterns = patterns('',
|
|||||||
# (r'^repo/removefetched/(?P<user_id>[^/]+)/(?P<repo_id>[^/]+)/$', remove_fetched_repo),
|
# (r'^repo/removefetched/(?P<user_id>[^/]+)/(?P<repo_id>[^/]+)/$', remove_fetched_repo),
|
||||||
# (r'^repo/setap/(?P<repo_id>[^/]+)/$', repo_set_access_property),
|
# (r'^repo/setap/(?P<repo_id>[^/]+)/$', repo_set_access_property),
|
||||||
url(r'^repo/(?P<repo_id>[^/]+)/(?P<obj_id>[^/]+)/$', repo_access_file, name='repo_access_file'),
|
url(r'^repo/(?P<repo_id>[^/]+)/(?P<obj_id>[^/]+)/$', repo_access_file, name='repo_access_file'),
|
||||||
(r'^repo/(?P<repo_id>[^/]+)/view/(?P<obj_id>[^/]+)/$', repo_view_file),
|
(r'^repo/(?P<repo_id>[^/]+)/files/$', repo_view_file),
|
||||||
|
(r'^repo/(?P<repo_id>[^/]+)/file/get/$', repo_file_get),
|
||||||
|
|
||||||
(r'^download/repo/$', repo_download),
|
(r'^download/repo/$', repo_download),
|
||||||
(r'^file/move/get_subdir/$', get_subdir),
|
(r'^file/move/get_subdir/$', get_subdir),
|
||||||
|
6
utils.py
6
utils.py
@@ -68,13 +68,13 @@ def get_ccnetapplet_root():
|
|||||||
ccnet_applet_root = settings.CCNET_APPLET_ROOT
|
ccnet_applet_root = settings.CCNET_APPLET_ROOT
|
||||||
return ccnet_applet_root
|
return ccnet_applet_root
|
||||||
|
|
||||||
def gen_token():
|
def gen_token(max_length=5):
|
||||||
"""
|
"""
|
||||||
Generate short token used for owner to access repo file.
|
Generate a random token.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
token = sha_constructor(settings.SECRET_KEY + unicode(time.time())).hexdigest()[:5]
|
token = sha_constructor(settings.SECRET_KEY + unicode(time.time())).hexdigest()[:max_length]
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def validate_group_name(group_name):
|
def validate_group_name(group_name):
|
||||||
|
343
views.py
343
views.py
@@ -24,6 +24,7 @@ from auth.decorators import login_required
|
|||||||
from auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, \
|
from auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, \
|
||||||
PasswordChangeForm
|
PasswordChangeForm
|
||||||
from auth.tokens import default_token_generator
|
from auth.tokens import default_token_generator
|
||||||
|
from share.models import FileShare
|
||||||
from seaserv import ccnet_rpc, ccnet_threaded_rpc, get_groups, get_users, get_repos, \
|
from seaserv import ccnet_rpc, ccnet_threaded_rpc, get_groups, get_users, get_repos, \
|
||||||
get_repo, get_commits, get_branches, \
|
get_repo, get_commits, get_branches, \
|
||||||
seafserv_threaded_rpc, seafserv_rpc, get_binding_peerids, get_ccnetuser, \
|
seafserv_threaded_rpc, seafserv_rpc, get_binding_peerids, get_ccnetuser, \
|
||||||
@@ -33,7 +34,7 @@ from pysearpc import SearpcError
|
|||||||
from seahub.base.accounts import CcnetUser
|
from seahub.base.accounts import CcnetUser
|
||||||
from seahub.contacts.models import Contact
|
from seahub.contacts.models import Contact
|
||||||
from seahub.notifications.models import UserNotification
|
from seahub.notifications.models import UserNotification
|
||||||
from forms import AddUserForm
|
from forms import AddUserForm, FileLinkShareForm
|
||||||
from utils import go_permission_error, go_error, list_to_string, \
|
from utils import go_permission_error, go_error, list_to_string, \
|
||||||
get_httpserver_root, get_ccnetapplet_root, gen_token, \
|
get_httpserver_root, get_ccnetapplet_root, gen_token, \
|
||||||
calculate_repo_last_modify, valid_previewed_file, \
|
calculate_repo_last_modify, valid_previewed_file, \
|
||||||
@@ -147,8 +148,9 @@ def gen_path_link(path, repo_name):
|
|||||||
Generate navigate paths and links in repo page.
|
Generate navigate paths and links in repo page.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if path[-1:] != '/':
|
if path and path[-1] != '/':
|
||||||
path += '/'
|
path += '/'
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
links = []
|
links = []
|
||||||
if path and path != '/':
|
if path and path != '/':
|
||||||
@@ -210,7 +212,6 @@ def render_repo(request, repo_id, error=''):
|
|||||||
|
|
||||||
# query repo infomation
|
# query repo infomation
|
||||||
repo_size = seafserv_threaded_rpc.server_repo_size(repo_id)
|
repo_size = seafserv_threaded_rpc.server_repo_size(repo_id)
|
||||||
# latest_commit = get_commits(repo_id, 0, 1)[0]
|
|
||||||
|
|
||||||
# get repo dirents
|
# get repo dirents
|
||||||
dirs = []
|
dirs = []
|
||||||
@@ -789,61 +790,37 @@ def repo_del_file(request, repo_id):
|
|||||||
url = reverse('repo', args=[repo_id]) + ('?p=%s' % parent_dir)
|
url = reverse('repo', args=[repo_id]) + ('?p=%s' % parent_dir)
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
def repo_view_file(request, repo_id, obj_id):
|
def repo_view_file(request, repo_id):
|
||||||
|
"""
|
||||||
|
Preview file on web, including files in current worktree and history.
|
||||||
|
"""
|
||||||
http_server_root = get_httpserver_root()
|
http_server_root = get_httpserver_root()
|
||||||
filename = urllib2.quote(request.GET.get('file_name', '').encode('utf-8'))
|
path = request.GET.get('p', '/')
|
||||||
|
if path[-1] != '/':
|
||||||
|
path = path + '/'
|
||||||
|
filename = urllib2.quote(os.path.basename(path[:-1]).encode('utf-8'))
|
||||||
|
|
||||||
commit_id = request.GET.get('commit_id', '')
|
commit_id = request.GET.get('commit_id', '')
|
||||||
view_history = True if commit_id else False
|
view_history = True if commit_id else False
|
||||||
current_commit = seafserv_threaded_rpc.get_commit(commit_id)
|
current_commit = seafserv_threaded_rpc.get_commit(commit_id)
|
||||||
if not current_commit:
|
if not current_commit:
|
||||||
current_commit = get_commits(repo_id, 0, 1)[0]
|
current_commit = get_commits(repo_id, 0, 1)[0]
|
||||||
|
|
||||||
if request.is_ajax():
|
if view_history:
|
||||||
content_type = 'application/json; charset=utf-8'
|
obj_id = request.GET.get('obj_id', '')
|
||||||
token = request.GET.get('t')
|
else:
|
||||||
tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s'
|
|
||||||
redirect_url = tmp_str % (http_server_root,
|
|
||||||
repo_id, obj_id,
|
|
||||||
filename, 'view',
|
|
||||||
token,
|
|
||||||
request.user.username)
|
|
||||||
try:
|
try:
|
||||||
proxied_request = urllib2.urlopen(redirect_url)
|
obj_id = seafserv_rpc.get_file_by_path(repo_id, path[:-1])
|
||||||
if long(proxied_request.headers['Content-Length']) > FILE_PREVIEW_MAX_SIZE:
|
except:
|
||||||
data = json.dumps([{'error': '文件超过10M,无法在线查看。'}])
|
obj_id = None
|
||||||
return HttpResponse(data, status=400, content_type=content_type)
|
|
||||||
else:
|
if not obj_id:
|
||||||
content = proxied_request.read()
|
return go_error(request, '文件不存在')
|
||||||
except urllib2.HTTPError, e:
|
|
||||||
err = 'HTTPError: 无法在线打开该文件'
|
|
||||||
data = json.dumps([{'error': err}])
|
|
||||||
return HttpResponse(data, status=400, content_type=content_type)
|
|
||||||
except urllib2.URLError as e:
|
|
||||||
err = 'URLError: 无法在线打开该文件'
|
|
||||||
data = json.dumps([{'error': err}])
|
|
||||||
return HttpResponse(data, status=400, content_type=content_type)
|
|
||||||
else:
|
|
||||||
l, d = [], {}
|
|
||||||
try:
|
|
||||||
# XXX: file in windows is encoded in gbk
|
|
||||||
u_content = content.decode('gbk')
|
|
||||||
except:
|
|
||||||
u_content = content.decode('utf-8')
|
|
||||||
from django.utils.html import escape
|
|
||||||
d['content'] = re.sub("\r\n|\n", "<br />", escape(u_content))
|
|
||||||
l.append(d)
|
|
||||||
data = json.dumps(l)
|
|
||||||
return HttpResponse(data, status=200, content_type=content_type)
|
|
||||||
|
|
||||||
repo = get_repo(repo_id)
|
repo = get_repo(repo_id)
|
||||||
if not repo:
|
if not repo:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
# if a repo doesn't have access property in db, then assume it's 'own'
|
|
||||||
repo_ap = seafserv_threaded_rpc.repo_query_access_property(repo_id)
|
|
||||||
if not repo_ap:
|
|
||||||
repo_ap = 'own'
|
|
||||||
|
|
||||||
# if a repo is shared to me, then I can view and download file no mater whether
|
# if a repo is shared to me, then I can view and download file no mater whether
|
||||||
# repo's access property is 'own' or 'public'
|
# repo's access property is 'own' or 'public'
|
||||||
if check_shared_repo(request, repo_id):
|
if check_shared_repo(request, repo_id):
|
||||||
@@ -852,30 +829,22 @@ def repo_view_file(request, repo_id, obj_id):
|
|||||||
share_to_me = False
|
share_to_me = False
|
||||||
|
|
||||||
token = ''
|
token = ''
|
||||||
if repo_ap == 'own':
|
# people who is owner or this repo is shared to him, can visit the repo;
|
||||||
# people who is owner or this repo is shared to him, can visit the repo;
|
# others, just go to 404 page
|
||||||
# others, just go to 404 page
|
if validate_owner(request, repo_id) or share_to_me:
|
||||||
if validate_owner(request, repo_id) or share_to_me:
|
# owner should get a token to visit repo
|
||||||
# owner should get a token to visit repo
|
token = gen_token()
|
||||||
token = gen_token()
|
# put token into memory in seaf-server
|
||||||
# put token into memory in seaf-server
|
seafserv_rpc.web_save_access_token(token, obj_id)
|
||||||
seafserv_rpc.web_save_access_token(token, obj_id)
|
else:
|
||||||
else:
|
raise Http404
|
||||||
raise Http404
|
|
||||||
|
|
||||||
# query commit info
|
|
||||||
commit_id = request.GET.get('commit_id', None)
|
|
||||||
current_commit = seafserv_threaded_rpc.get_commit(commit_id)
|
|
||||||
if not current_commit:
|
|
||||||
current_commit = get_commits(repo.id, 0, 1)[0]
|
|
||||||
|
|
||||||
# generate path and link
|
# generate path and link
|
||||||
path = request.GET.get('p', '/')
|
|
||||||
zipped = gen_path_link(path, repo.name)
|
zipped = gen_path_link(path, repo.name)
|
||||||
|
|
||||||
# filename
|
# determin whether file can preview on web
|
||||||
can_preview, filetype = valid_previewed_file(filename)
|
can_preview, filetype = valid_previewed_file(filename)
|
||||||
|
|
||||||
# raw path
|
# raw path
|
||||||
tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s'
|
tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s'
|
||||||
raw_path = tmp_str % (http_server_root,
|
raw_path = tmp_str % (http_server_root,
|
||||||
@@ -883,12 +852,26 @@ def repo_view_file(request, repo_id, obj_id):
|
|||||||
filename, 'view',
|
filename, 'view',
|
||||||
token,
|
token,
|
||||||
request.user.username)
|
request.user.username)
|
||||||
|
|
||||||
|
# file share link
|
||||||
|
l = FileShare.objects.filter(repo_id=repo_id).filter(path=path[:-1])
|
||||||
|
fileshare = l[0] if len(l) > 0 else None
|
||||||
|
|
||||||
|
http_or_https = request.is_secure() and 'https' or 'http'
|
||||||
|
domain = RequestSite(request).domain
|
||||||
|
if fileshare:
|
||||||
|
file_shared_link = '%s://%s%sf/%s/' % (http_or_https, domain,
|
||||||
|
settings.SITE_ROOT,
|
||||||
|
fileshare.token)
|
||||||
|
else:
|
||||||
|
file_shared_link = ''
|
||||||
|
|
||||||
return render_to_response('repo_view_file.html', {
|
return render_to_response('repo_view_file.html', {
|
||||||
'repo': repo,
|
'repo': repo,
|
||||||
'path': path,
|
'path': path,
|
||||||
'obj_id': obj_id,
|
'obj_id': obj_id,
|
||||||
'file_name': filename,
|
'file_name': filename,
|
||||||
|
'path': path,
|
||||||
'zipped': zipped,
|
'zipped': zipped,
|
||||||
'view_history': view_history,
|
'view_history': view_history,
|
||||||
'current_commit': current_commit,
|
'current_commit': current_commit,
|
||||||
@@ -896,8 +879,76 @@ def repo_view_file(request, repo_id, obj_id):
|
|||||||
'can_preview': can_preview,
|
'can_preview': can_preview,
|
||||||
'filetype': filetype,
|
'filetype': filetype,
|
||||||
'raw_path': raw_path,
|
'raw_path': raw_path,
|
||||||
|
'fileshare': fileshare,
|
||||||
|
'protocol': http_or_https,
|
||||||
|
'domain': domain,
|
||||||
|
'file_shared_link': file_shared_link,
|
||||||
}, context_instance=RequestContext(request))
|
}, context_instance=RequestContext(request))
|
||||||
|
|
||||||
|
def repo_file_get(request, repo_id):
|
||||||
|
"""
|
||||||
|
Handle ajax request to get file content from httpserver.
|
||||||
|
If get current worktree file, need access_token, path and username from
|
||||||
|
url params.
|
||||||
|
If get history file, need access_token, path username and obj_id from
|
||||||
|
url params.
|
||||||
|
"""
|
||||||
|
if not request.is_ajax():
|
||||||
|
return Http404
|
||||||
|
|
||||||
|
http_server_root = get_httpserver_root()
|
||||||
|
content_type = 'application/json; charset=utf-8'
|
||||||
|
access_token = request.GET.get('t')
|
||||||
|
path = request.GET.get('p', '/')
|
||||||
|
if path[-1] == '/':
|
||||||
|
path = path[:-1]
|
||||||
|
|
||||||
|
filename = urllib2.quote(os.path.basename(path).encode('utf-8'))
|
||||||
|
obj_id = request.GET.get('obj_id', '')
|
||||||
|
if not obj_id:
|
||||||
|
try:
|
||||||
|
obj_id = seafserv_rpc.get_file_by_path(repo_id, path)
|
||||||
|
except:
|
||||||
|
obj_id = None
|
||||||
|
if not obj_id:
|
||||||
|
data = json.dumps([{'error': '获取文件数据失败'}])
|
||||||
|
return HttpResponse(data, status=400, content_type=content_type)
|
||||||
|
|
||||||
|
username = request.GET.get('u', '')
|
||||||
|
tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s'
|
||||||
|
redirect_url = tmp_str % (http_server_root,
|
||||||
|
repo_id, obj_id,
|
||||||
|
filename, 'view',
|
||||||
|
access_token,
|
||||||
|
username)
|
||||||
|
try:
|
||||||
|
proxied_request = urllib2.urlopen(redirect_url)
|
||||||
|
if long(proxied_request.headers['Content-Length']) > FILE_PREVIEW_MAX_SIZE:
|
||||||
|
data = json.dumps([{'error': '文件超过10M,无法在线查看。'}])
|
||||||
|
return HttpResponse(data, status=400, content_type=content_type)
|
||||||
|
else:
|
||||||
|
content = proxied_request.read()
|
||||||
|
except urllib2.HTTPError, e:
|
||||||
|
err = 'HTTPError: 无法在线打开该文件'
|
||||||
|
data = json.dumps([{'error': err}])
|
||||||
|
return HttpResponse(data, status=400, content_type=content_type)
|
||||||
|
except urllib2.URLError as e:
|
||||||
|
err = 'URLError: 无法在线打开该文件'
|
||||||
|
data = json.dumps([{'error': err}])
|
||||||
|
return HttpResponse(data, status=400, content_type=content_type)
|
||||||
|
else:
|
||||||
|
l, d = [], {}
|
||||||
|
try:
|
||||||
|
# XXX: file in windows is encoded in gbk
|
||||||
|
u_content = content.decode('gbk')
|
||||||
|
except:
|
||||||
|
u_content = content.decode('utf-8')
|
||||||
|
from django.utils.html import escape
|
||||||
|
d['content'] = re.sub("\r\n|\n", "<br />", escape(u_content))
|
||||||
|
l.append(d)
|
||||||
|
data = json.dumps(l)
|
||||||
|
return HttpResponse(data, status=200, content_type=content_type)
|
||||||
|
|
||||||
def repo_access_file(request, repo_id, obj_id):
|
def repo_access_file(request, repo_id, obj_id):
|
||||||
if repo_id:
|
if repo_id:
|
||||||
repo = get_repo(repo_id)
|
repo = get_repo(repo_id)
|
||||||
@@ -1756,3 +1807,163 @@ def file_revisions(request, repo_id):
|
|||||||
% (commit_id, file_name, path)
|
% (commit_id, file_name, path)
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def get_shared_link(request):
|
||||||
|
"""
|
||||||
|
Handle ajax request to generate file shared link.
|
||||||
|
"""
|
||||||
|
if not request.is_ajax():
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
content_type = 'application/json; charset=utf-8'
|
||||||
|
|
||||||
|
repo_id = request.GET.get('repo_id')
|
||||||
|
obj_id = request.GET.get('obj_id')
|
||||||
|
path = request.GET.get('p', '/')
|
||||||
|
if path[-1] == '/':
|
||||||
|
path = path[:-1]
|
||||||
|
|
||||||
|
l = FileShare.objects.filter(repo_id=repo_id).filter(path=path)
|
||||||
|
if len(l) > 0:
|
||||||
|
fileshare = l[0]
|
||||||
|
token = fileshare.token
|
||||||
|
else:
|
||||||
|
token = gen_token(max_length=10)
|
||||||
|
|
||||||
|
fs = FileShare()
|
||||||
|
fs.username = request.user.username
|
||||||
|
fs.repo_id = repo_id
|
||||||
|
fs.path = path
|
||||||
|
fs.token = token
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs.save()
|
||||||
|
except IntegrityError, e:
|
||||||
|
err = '获取分享链接失败,请重新获取'
|
||||||
|
data = json.dumps([{'error': err}])
|
||||||
|
return HttpResponse(data, status=500, content_type=content_type)
|
||||||
|
|
||||||
|
data = json.dumps([{'token': token}])
|
||||||
|
return HttpResponse(data, status=200, content_type=content_type)
|
||||||
|
|
||||||
|
def view_shared_file(request, token):
|
||||||
|
"""
|
||||||
|
Preview file via shared link.
|
||||||
|
"""
|
||||||
|
assert token is not None # Checked by URLconf
|
||||||
|
|
||||||
|
try:
|
||||||
|
fileshare = FileShare.objects.get(token=token)
|
||||||
|
except FileShare.DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
username = fileshare.username
|
||||||
|
repo_id = fileshare.repo_id
|
||||||
|
path = fileshare.path
|
||||||
|
|
||||||
|
http_server_root = get_httpserver_root()
|
||||||
|
if path[-1] == '/':
|
||||||
|
path = path[:-1]
|
||||||
|
filename = os.path.basename(path)
|
||||||
|
quote_filename = urllib2.quote(filename.encode('utf-8'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj_id = seafserv_rpc.get_file_by_path(repo_id, path)
|
||||||
|
except:
|
||||||
|
obj_id = None
|
||||||
|
|
||||||
|
if not obj_id:
|
||||||
|
return go_error(request, '文件不存在')
|
||||||
|
|
||||||
|
repo = get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
access_token = gen_token()
|
||||||
|
seafserv_rpc.web_save_access_token(access_token, obj_id)
|
||||||
|
|
||||||
|
can_preview, filetype = valid_previewed_file(filename)
|
||||||
|
|
||||||
|
# raw path
|
||||||
|
tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s'
|
||||||
|
raw_path = tmp_str % (http_server_root,
|
||||||
|
repo_id, obj_id,
|
||||||
|
quote_filename, 'view',
|
||||||
|
access_token,
|
||||||
|
username)
|
||||||
|
|
||||||
|
return render_to_response('view_shared_file.html', {
|
||||||
|
'repo': repo,
|
||||||
|
'obj_id': obj_id,
|
||||||
|
'path': path,
|
||||||
|
'file_name': filename,
|
||||||
|
'token': token,
|
||||||
|
'access_token': access_token,
|
||||||
|
'can_preview': can_preview,
|
||||||
|
'filetype': filetype,
|
||||||
|
'raw_path': raw_path,
|
||||||
|
'username': username,
|
||||||
|
}, context_instance=RequestContext(request))
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def remove_shared_link(request):
|
||||||
|
"""
|
||||||
|
Handle ajax request to remove file shared link.
|
||||||
|
"""
|
||||||
|
if not request.is_ajax():
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
content_type = 'application/json; charset=utf-8'
|
||||||
|
|
||||||
|
token = request.GET.get('t', '')
|
||||||
|
FileShare.objects.filter(token=token).delete()
|
||||||
|
|
||||||
|
msg = '删除成功'
|
||||||
|
data = json.dumps([{'msg': msg}])
|
||||||
|
return HttpResponse(data, status=200, content_type=content_type)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def send_shared_link(request):
|
||||||
|
"""
|
||||||
|
Handle ajax post request to share file shared link.
|
||||||
|
"""
|
||||||
|
if not request.is_ajax() and not request.method == 'POST':
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
content_type = 'application/json; charset=utf-8'
|
||||||
|
|
||||||
|
form = FileLinkShareForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
err = '发送失败'
|
||||||
|
data = json.dumps([{'error':err}])
|
||||||
|
return HttpResponse(data, status=400, content_type=content_type)
|
||||||
|
|
||||||
|
email = form.cleaned_data['email']
|
||||||
|
file_shared_link = form.cleaned_data['file_shared_link']
|
||||||
|
|
||||||
|
# Handle the diffent separator
|
||||||
|
to_email_str = email.replace(';',',')
|
||||||
|
to_email_str = to_email_str.replace('\n',',')
|
||||||
|
to_email_str = to_email_str.replace('\r',',')
|
||||||
|
to_email_list = to_email_str.split(',')
|
||||||
|
|
||||||
|
t = loader.get_template('shared_link_email.html')
|
||||||
|
for to_email in to_email_list:
|
||||||
|
c = {
|
||||||
|
'email': request.user.username,
|
||||||
|
'to_email': to_email,
|
||||||
|
'file_shared_link': file_shared_link,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
send_mail('您的好友通过SeaCloud分享了一个文件给您',
|
||||||
|
t.render(Context(c)), None, [to_email],
|
||||||
|
fail_silently=False)
|
||||||
|
except:
|
||||||
|
err = '发送失败'
|
||||||
|
data = json.dumps([{'error':err}])
|
||||||
|
return HttpResponse(data, status=400, content_type=content_type)
|
||||||
|
|
||||||
|
msg = '发送成功。'
|
||||||
|
data = json.dumps([{'msg': msg}])
|
||||||
|
return HttpResponse(data, status=200, content_type=content_type)
|
||||||
|
Reference in New Issue
Block a user