mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-01 23:38:37 +00:00
new devices page
This commit is contained in:
parent
585b48e3c4
commit
a43b3f969e
@ -10,3 +10,4 @@ django-statici18n==1.1.2
|
||||
djangorestframework==3.3.1
|
||||
git+git://github.com/haiwen/django-constance.git@bde7f7cdfd0ed1631a6817fd4cd76f37bf54fe35#egg=django-constance[database]
|
||||
openpyxl==2.3.0
|
||||
pytz==2015.7
|
||||
|
@ -2,7 +2,6 @@ import json
|
||||
|
||||
from django.core.paginator import EmptyPage, InvalidPage
|
||||
from django.http import HttpResponse
|
||||
from django.utils.dateformat import DateFormat
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
@ -16,6 +15,7 @@ from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.group.models import GroupMessage
|
||||
from seahub.utils.paginator import Paginator
|
||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||
from .utils import api_check_group
|
||||
|
||||
json_content_type = 'application/json; charset=utf-8'
|
||||
@ -55,12 +55,13 @@ class GroupDiscussions(APIView):
|
||||
|
||||
msgs = []
|
||||
for e in group_msgs:
|
||||
isoformat_timestr = datetime_to_isoformat_timestr(e.timestamp)
|
||||
msgs.append({
|
||||
"group_id": group_id,
|
||||
"discussion_id": e.pk,
|
||||
"user": e.from_email,
|
||||
"content": e.message,
|
||||
"created_at": e.timestamp.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(e.timestamp).format('O'),
|
||||
"created_at": isoformat_timestr,
|
||||
})
|
||||
|
||||
return HttpResponse(json.dumps(msgs), status=200,
|
||||
@ -79,10 +80,11 @@ class GroupDiscussions(APIView):
|
||||
from_email=username,
|
||||
message=content)
|
||||
|
||||
isoformat_timestr = datetime_to_isoformat_timestr(discuss.timestamp)
|
||||
return Response({
|
||||
"group_id": group_id,
|
||||
"discussion_id": discuss.pk,
|
||||
"user": username,
|
||||
"content": discuss.message,
|
||||
"created_at": discuss.timestamp.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(discuss.timestamp).format('O'),
|
||||
"created_at": isoformat_timestr,
|
||||
}, status=201)
|
||||
|
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
from django.utils.dateformat import DateFormat
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
|
||||
@ -21,7 +20,7 @@ from seahub.avatar.settings import GROUP_AVATAR_DEFAULT_SIZE
|
||||
from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \
|
||||
get_default_group_avatar_url
|
||||
from seahub.utils import is_org_context, is_valid_username
|
||||
from seahub.utils.timeutils import dt, utc_to_local
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||
from seahub.group.utils import validate_group_name, check_group_name_conflict, \
|
||||
is_group_member, is_group_admin, is_group_owner, is_group_admin_or_owner
|
||||
from seahub.group.views import remove_group_common
|
||||
@ -52,12 +51,12 @@ def get_group_info(request, group_id, avatar_size=GROUP_AVATAR_DEFAULT_SIZE):
|
||||
logger.error(e)
|
||||
avatar_url = get_default_group_avatar_url()
|
||||
|
||||
val = utc_to_local(dt(group.timestamp))
|
||||
isoformat_timestr = timestamp_to_isoformat_timestr(group.timestamp)
|
||||
group_info = {
|
||||
"id": group.id,
|
||||
"name": group.group_name,
|
||||
"owner": group.creator_name,
|
||||
"created_at": val.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(val).format('O'),
|
||||
"created_at": isoformat_timestr,
|
||||
"avatar_url": request.build_absolute_uri(avatar_url),
|
||||
"admins": get_group_admins(group.id),
|
||||
"wiki_enabled": is_wiki_mod_enabled_for_group(group_id)
|
||||
|
@ -2,7 +2,6 @@ import json
|
||||
import os
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.utils.dateformat import DateFormat
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
@ -13,6 +12,7 @@ from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.share.models import UploadLinkShare
|
||||
from seahub.utils import gen_shared_upload_link
|
||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||
|
||||
json_content_type = 'application/json; charset=utf-8'
|
||||
|
||||
@ -45,7 +45,7 @@ class SharedUploadLinksView(APIView):
|
||||
link.repo = r
|
||||
|
||||
if link.expire_date:
|
||||
expire_date = link.expire_date.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(link.expire_date).format('O')
|
||||
expire_date = datetime_to_isoformat_timestr(link.expire_date)
|
||||
else:
|
||||
expire_date = ""
|
||||
|
||||
@ -54,7 +54,7 @@ class SharedUploadLinksView(APIView):
|
||||
"repo_id": link.repo_id,
|
||||
"path": link.path,
|
||||
"token": link.token,
|
||||
"ctime": link.ctime.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(link.ctime).format('O'),
|
||||
"ctime": datetime_to_isoformat_timestr(link.ctime),
|
||||
"view_cnt": link.view_cnt,
|
||||
"expire_date": expire_date,
|
||||
})
|
||||
|
@ -61,6 +61,7 @@ urlpatterns = patterns('',
|
||||
url(r'^repos/(?P<repo_id>[-0-9-a-f]{36})/dir/download/$', DirDownloadView.as_view(), name='api2-dir-download'),
|
||||
url(r'^repos/(?P<repo_id>[-0-9-a-f]{36})/thumbnail/$', ThumbnailView.as_view(), name='api2-thumbnail'),
|
||||
url(r'^starredfiles/', StarredFileView.as_view(), name='starredfiles'),
|
||||
url(r'^devices/', DevicesView.as_view(), name='api2-devices'),
|
||||
url(r'^shared-repos/$', SharedRepos.as_view(), name='sharedrepos'),
|
||||
url(r'^shared-repos/(?P<repo_id>[-0-9-a-f]{36})/$', SharedRepo.as_view(), name='sharedrepo'),
|
||||
url(r'^beshared-repos/$', BeShared.as_view(), name='beshared'),
|
||||
|
@ -29,7 +29,6 @@ from django.template.defaultfilters import filesizeformat
|
||||
from django.shortcuts import render_to_response
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.dateformat import DateFormat
|
||||
|
||||
from .throttling import ScopedRateThrottle, AnonRateThrottle, UserRateThrottle
|
||||
from .authentication import TokenAuthentication
|
||||
@ -75,11 +74,12 @@ from seahub.utils import gen_file_get_url, gen_token, gen_file_upload_url, \
|
||||
gen_file_share_link, gen_dir_share_link, is_org_context, gen_shared_link, \
|
||||
get_org_user_events, calculate_repos_last_modify, send_perm_audit_msg, \
|
||||
gen_shared_upload_link, convert_cmmt_desc_link, is_org_repo_creation_allowed
|
||||
from seahub.utils.devices import get_user_devices, do_unlink_device
|
||||
from seahub.utils.repo import get_sub_repo_abbrev_origin_path
|
||||
from seahub.utils.star import star_file, unstar_file
|
||||
from seahub.utils.file_types import IMAGE, DOCUMENT
|
||||
from seahub.utils.file_size import get_file_size_unit
|
||||
from seahub.utils.timeutils import utc_to_local
|
||||
from seahub.utils.timeutils import utc_to_local, datetime_to_isoformat_timestr
|
||||
from seahub.views import validate_owner, is_registered_user, check_file_lock, \
|
||||
group_events_data, get_diff, create_default_library, get_owned_repo_list, \
|
||||
list_inner_pub_repos, get_virtual_repos_by_owner, \
|
||||
@ -1861,6 +1861,38 @@ class OwaFileView(APIView):
|
||||
send_file_access_msg(request, repo, path, 'api')
|
||||
return Response(wopi_dict)
|
||||
|
||||
class DevicesView(APIView):
|
||||
"""List user devices"""
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def get(self, request, format=None):
|
||||
username = request.user.username
|
||||
user_devices = get_user_devices(username)
|
||||
return Response(user_devices)
|
||||
|
||||
def delete(self, request, format=None):
|
||||
|
||||
platform = request.data.get('platform', '')
|
||||
device_id = request.data.get('device_id', '')
|
||||
|
||||
if not platform:
|
||||
error_msg = 'platform invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
if not device_id:
|
||||
error_msg = 'device_id invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
try:
|
||||
do_unlink_device(request.user.username, platform, device_id)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
return Response({'success': True})
|
||||
|
||||
class FileView(APIView):
|
||||
"""
|
||||
@ -4327,8 +4359,7 @@ class RepoDownloadSharedLinks(APIView):
|
||||
|
||||
shared_link['create_by'] = fs.username
|
||||
shared_link['creator_name'] = email2nickname(fs.username)
|
||||
# return time with time zone: 2016-01-18T15:03:10+0800
|
||||
shared_link['create_time'] = fs.ctime.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(fs.ctime).format('O')
|
||||
shared_link['create_time'] = datetime_to_isoformat_timestr(fs.ctime)
|
||||
shared_link['token'] = fs.token
|
||||
shared_link['path'] = path
|
||||
shared_link['name'] = os.path.basename(path.rstrip('/')) if path != '/' else '/'
|
||||
@ -4412,8 +4443,7 @@ class RepoUploadSharedLinks(APIView):
|
||||
|
||||
shared_link['create_by'] = fs.username
|
||||
shared_link['creator_name'] = email2nickname(fs.username)
|
||||
# return time with time zone: 2016-01-18T15:03:10+0800
|
||||
shared_link['create_time'] = fs.ctime.strftime("%Y-%m-%dT%H:%M:%S") + DateFormat(fs.ctime).format('O')
|
||||
shared_link['create_time'] = datetime_to_isoformat_timestr(fs.ctime)
|
||||
shared_link['token'] = fs.token
|
||||
shared_link['path'] = path
|
||||
shared_link['name'] = os.path.basename(path.rstrip('/')) if path != '/' else '/'
|
||||
|
@ -1,120 +0,0 @@
|
||||
{% extends "home_base.html" %}
|
||||
|
||||
{% load seahub_tags avatar_tags i18n %}
|
||||
|
||||
{% block sub_title %}{% trans "Devices" %} - {% endblock %}
|
||||
{% block cur_devices %}tab-cur{% endblock %}
|
||||
|
||||
{% block right_panel %}
|
||||
<h3 class="hd">{% trans "Devices" %}</h3>
|
||||
{% if devices %}
|
||||
<table class="client-list">
|
||||
<tr>
|
||||
<th width="13%">{% trans "Platform" %}</th>
|
||||
<th width="25%">{% trans "Device Name" %}</th>
|
||||
<th width="20%">{% trans "IP" %}</th>
|
||||
<th width="17%">{% trans "Last Access" %}</th>
|
||||
<th width="15%">{% trans "# Libraries" %}</th>
|
||||
<th width="10%"></th>
|
||||
</tr>
|
||||
{% for device in devices %}
|
||||
<tr data-platform="{{ device.platform }}" data-name="{{ device.device_name }}" data-device-id="{{ device.device_id }}">
|
||||
<td>{{ device.platform }}</td>
|
||||
<td>{{ device.device_name }}</td>
|
||||
<td>{{ device.last_login_ip }}</td>
|
||||
<td>{{ device.last_accessed | translate_seahub_time }}</td>
|
||||
<td>
|
||||
<span class="lib-num">{{ device.synced_repos|length }}{% if device.synced_repos %} <span class="dir-icon icon-caret-down"></span>{% endif %}</span>
|
||||
{% if device.synced_repos %}
|
||||
<ul class="lib-list hide">
|
||||
{% for repo in device.synced_repos %}
|
||||
<li><a href="{% url 'view_common_lib_dir' repo.repo_id '' %}">{{ repo.repo_name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<span class="unlink-device op-icon sf2-icon-delete vh" title="{% trans "Unlink" %}"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="op-confirm unsync-confirm hide" id="unsync-cfm-popup">
|
||||
<p class="details">{% trans "Really want to unlink %s?" %}</p>
|
||||
<p>{% trans "It will immediately stop syncing." %}</p>
|
||||
<button class="yes">{% trans "Yes" %}</button>
|
||||
<button class="no">{% trans "No" %}</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-tips">
|
||||
<h2 class="alc">{% trans "You do not have connected devices" %}</h2>
|
||||
<p>{% trans "Your clients (Desktop/Android/iOS) will be listed here." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}{{block.super}}
|
||||
<script type="text/javascript">
|
||||
$('.lib-num').click(function(){
|
||||
var lib_list = $(this).next('.lib-list');
|
||||
var dir_icon = $(this).children('.dir-icon');
|
||||
if (lib_list.length > 0) {
|
||||
lib_list.toggleClass('hide');
|
||||
if (lib_list.hasClass('hide')) {
|
||||
dir_icon.removeClass('icon-caret-up').addClass('icon-caret-down');
|
||||
} else {
|
||||
dir_icon.removeClass('icon-caret-down').addClass('icon-caret-up');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
$('#main-panel').removeClass('ovhd');
|
||||
$('.unlink-device').click(function() {
|
||||
var op = $(this);
|
||||
var cont = op.parent().css({'position': 'relative'}),
|
||||
cfm;
|
||||
var tr = op.parents('tr');
|
||||
|
||||
// only show 1 popup each time.
|
||||
$('.unsync-confirm', op.parents('table')).addClass('hide');
|
||||
|
||||
if (cont.find('.unsync-confirm').length == 1) {
|
||||
cfm = cont.find('.unsync-confirm');
|
||||
} else {
|
||||
cfm = $('#unsync-cfm-popup').clone().removeAttr('id');
|
||||
cont.append(cfm);
|
||||
cfm.css({'right':0, 'top': op.position().top + op.height() + 3, 'white-space':'nowrap'});
|
||||
}
|
||||
cfm.removeClass('hide');
|
||||
|
||||
var details = $('.details', cfm);
|
||||
details.html(details.html().replace('%s', tr.attr('data-name')));
|
||||
|
||||
$('.no',cfm).unbind().click(function() {
|
||||
cfm.addClass('hide');
|
||||
});
|
||||
$('.yes',cfm).unbind().click(function() {
|
||||
cfm.addClass('hide');
|
||||
$.ajax({
|
||||
url: '{% url 'unlink_device' %}',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
beforeSend: prepareCSRFToken,
|
||||
data: {
|
||||
'platform': tr.data('platform'),
|
||||
'device_id': tr.data('device-id')
|
||||
},
|
||||
success: function() {
|
||||
tr.remove();
|
||||
feedback("{% trans "Successfully unlinked." %}", 'success');
|
||||
},
|
||||
error: ajaxErrorHandler
|
||||
});
|
||||
});
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -50,7 +50,7 @@
|
||||
<li class="tab {% block cur_personal_wiki %}{% endblock %}"><a href="{% url 'personal_wiki' %}"><span class="sf2-icon-wiki"></span>{% trans "Personal Wiki" %}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li class="tab {% block cur_devices %}{% endblock %}"><a href="{% url 'devices' %}"><span class="sf2-icon-monitor"></span>{% trans "Devices" %}</a></li>
|
||||
<li class="tab {% block cur_devices %}{% endblock %}"><a href="{{ SITE_ROOT }}#devices/"><span class="sf2-icon-monitor"></span>{% trans "Devices" %}</a></li>
|
||||
</ul>
|
||||
|
||||
<h3 class="hd">{% trans "Share Admin" %}</h3>
|
||||
|
@ -666,7 +666,7 @@
|
||||
<li class="tab"><a href="{% url 'personal_wiki' %}"><span class="sf2-icon-wiki"></span>{% trans "Personal Wiki" %}</a></li>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<li class="tab"><a href="{% url 'devices' %}"><span class="sf2-icon-monitor"></span>{% trans "Devices" %}</a></li>
|
||||
<li class="tab<% if (cur_tab == 'devices') { %> tab-cur<% } %>"><a href="{{ SITE_ROOT }}#devices/"><span class="sf2-icon-monitor"></span>{% trans "Devices" %}</a></li>
|
||||
</ul>
|
||||
|
||||
<h3 class="hd">{% trans "Share Admin" %}</h3>
|
||||
@ -845,6 +845,27 @@
|
||||
<span class="sf2-icon-delete unstar op-icon vh" title="{% trans "Unstar" %}"></span>
|
||||
</td>
|
||||
</script>
|
||||
<script type="text/template" id="device-item-tmpl">
|
||||
<td><%- platform %></td>
|
||||
<td><%- device_name %></td>
|
||||
<td><%- last_login_ip %></td>
|
||||
<td><time title='<%- time %>'><%- time_from_now %></time></td>
|
||||
<td>
|
||||
<span class="lib-num cspt"><%- synced_repos_length %><% if (synced_repos.length > 0) { %> <span class="dir-icon icon-caret-down"></span><% } %></span>
|
||||
<% if (synced_repos.length > 0) { %>
|
||||
<ul class="lib-list hide">
|
||||
<% for (var i = 0, len = synced_repos_length; i < len; i++) { %>
|
||||
<li><a href="#my-libs/lib/<%- synced_repos[i].repo_id %>"><%- synced_repos[i].repo_name %></a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<span class="unlink-device op-icon sf2-icon-delete vh" title="{% trans "Unlink" %}"></span>
|
||||
</div>
|
||||
</td>
|
||||
</script>
|
||||
<script type="text/template" id="my-repos-hd-tmpl">
|
||||
<tr>
|
||||
<th width="4%"><!--icon--></th>
|
||||
|
@ -75,6 +75,28 @@
|
||||
<button id="activities-more" class="hide">{% trans 'More' %}</button>
|
||||
</div>
|
||||
|
||||
<div id="devices" class="hide">
|
||||
<h3 class="hd">{% trans "Devices" %}</h3>
|
||||
<table class="hide">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="13%">{% trans "Platform" %}</th>
|
||||
<th width="25%">{% trans "Device Name" %}</th>
|
||||
<th width="20%">{% trans "IP" %}</th>
|
||||
<th width="17%">{% trans "Last Access" %}</th>
|
||||
<th width="15%">{% trans "# Libraries" %}</th>
|
||||
<th width="10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<span class="loading-icon loading-tip"></span>
|
||||
<div class="empty-tips hide">
|
||||
<h2 class="alc">{% trans "You do not have connected devices" %}</h2>
|
||||
<p>{% trans "Your clients (Desktop/Android/iOS) will be listed here." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="guide-for-new" class="hide">
|
||||
<span class="icon-lightbulb fleft"></span>
|
||||
<div class="txt">
|
||||
@ -232,6 +254,7 @@ app["pageOptions"] = {
|
||||
enable_thumbnail: {% if enable_thumbnail %} true {% else %} false {% endif %},
|
||||
thumbnail_default_size: {{ thumbnail_default_size }},
|
||||
thumbnail_size_for_grid: {{ thumbnail_size_for_grid }},
|
||||
language_code: "{{ LANGUAGE_CODE }}",
|
||||
enable_encrypted_library: {% if enable_encrypted_library %} true {% else %} false {% endif %},
|
||||
enable_repo_history_setting: {% if enable_repo_history_setting %} true {% else %} false {% endif %},
|
||||
max_upload_file_size: {% if max_upload_file_size %} {{ max_upload_file_size }} {% else %} '' {% endif %},
|
||||
|
@ -56,9 +56,6 @@ urlpatterns = patterns(
|
||||
url(r'^home/wiki_page_edit/(?P<page_name>[^/]+)$', personal_wiki_page_edit, name='personal_wiki_page_edit'),
|
||||
url(r'^home/wiki_page_delete/(?P<page_name>[^/]+)$', personal_wiki_page_delete, name='personal_wiki_page_delete'),
|
||||
|
||||
url(r'^devices/$', devices, name='devices'),
|
||||
url(r'^home/devices/unlink/$', unlink_device, name='unlink_device'),
|
||||
|
||||
# url(r'^home/public/reply/(?P<msg_id>[\d]+)/$', innerpub_msg_reply, name='innerpub_msg_reply'),
|
||||
# url(r'^home/owner/(?P<owner_name>[^/]+)/$', ownerhome, name='ownerhome'),
|
||||
|
||||
|
@ -3,6 +3,7 @@ import datetime
|
||||
|
||||
from seaserv import seafile_api
|
||||
from seahub.api2.models import TokenV2, DESKTOP_PLATFORMS
|
||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -17,7 +18,6 @@ def _last_sync_time(repos):
|
||||
|
||||
def get_user_devices(username):
|
||||
devices = TokenV2.objects.get_user_devices(username)
|
||||
|
||||
peer_repos_map = get_user_synced_repo_infos(username)
|
||||
|
||||
for device in devices:
|
||||
@ -29,6 +29,7 @@ def get_user_devices(username):
|
||||
device['last_accessed'] = max(device['last_accessed'],
|
||||
_last_sync_time(repos))
|
||||
|
||||
device['last_accessed'] = datetime_to_isoformat_timestr(device['last_accessed'])
|
||||
return devices
|
||||
|
||||
def get_user_synced_repo_infos(username):
|
||||
|
@ -1,3 +1,4 @@
|
||||
import pytz
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
from django.utils import six
|
||||
@ -11,7 +12,6 @@ def dt(value):
|
||||
except ValueError:
|
||||
# TODO: need a better way to handle 64 bits timestamp.
|
||||
return datetime.datetime.utcfromtimestamp(value/1000000)
|
||||
|
||||
|
||||
def value_to_db_datetime(value):
|
||||
if value is None:
|
||||
@ -33,3 +33,16 @@ def utc_to_local(dt):
|
||||
utc = dt.replace(tzinfo=timezone.utc)
|
||||
local = timezone.make_naive(utc, tz)
|
||||
return local
|
||||
|
||||
def timestamp_to_isoformat_timestr(timestamp):
|
||||
dt_obj = dt(timestamp)
|
||||
dt_obj = dt_obj.replace(microsecond=0)
|
||||
pytz_obj = pytz.timezone(settings.TIME_ZONE)
|
||||
isoformat_timestr = pytz_obj.localize(dt_obj).isoformat()
|
||||
return isoformat_timestr
|
||||
|
||||
def datetime_to_isoformat_timestr(datetime):
|
||||
datetime = datetime.replace(microsecond=0)
|
||||
pytz_obj = pytz.timezone(settings.TIME_ZONE)
|
||||
isoformat_timestr = pytz_obj.localize(datetime).isoformat()
|
||||
return isoformat_timestr
|
||||
|
@ -60,7 +60,6 @@ from seahub.utils.star import get_dir_starred_files
|
||||
from seahub.utils.timeutils import utc_to_local
|
||||
from seahub.views.modules import MOD_PERSONAL_WIKI, enable_mod_for_user, \
|
||||
disable_mod_for_user
|
||||
from seahub.utils.devices import get_user_devices, do_unlink_device
|
||||
import seahub.settings as settings
|
||||
from seahub.settings import FILE_PREVIEW_MAX_SIZE, INIT_PASSWD, USE_PDFJS, \
|
||||
FILE_ENCODING_LIST, FILE_ENCODING_TRY_LIST, AVATAR_FILE_STORAGE, \
|
||||
@ -886,37 +885,6 @@ def libraries(request):
|
||||
'can_add_pub_repo': can_add_pub_repo,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
@login_required
|
||||
@user_mods_check
|
||||
def devices(request):
|
||||
"""List user devices"""
|
||||
username = request.user.username
|
||||
user_devices = get_user_devices(username)
|
||||
|
||||
return render_to_response('devices.html', {
|
||||
"devices": user_devices,
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
@login_required_ajax
|
||||
@require_POST
|
||||
def unlink_device(request):
|
||||
content_type = 'application/json; charset=utf-8'
|
||||
|
||||
platform = request.POST.get('platform', '')
|
||||
device_id = request.POST.get('device_id', '')
|
||||
|
||||
if not platform or not device_id:
|
||||
return HttpResponseBadRequest(json.dumps({'error': _(u'Argument missing')}),
|
||||
content_type=content_type)
|
||||
|
||||
try:
|
||||
do_unlink_device(request.user.username, platform, device_id)
|
||||
except:
|
||||
return HttpResponse(json.dumps({'error': _(u'Internal server error')}),
|
||||
status=500, content_type=content_type)
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}), content_type=content_type)
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def unsetinnerpub(request, repo_id):
|
||||
|
17
static/scripts/app/collections/devices.js
Normal file
17
static/scripts/app/collections/devices.js
Normal file
@ -0,0 +1,17 @@
|
||||
define([
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common',
|
||||
'app/models/device'
|
||||
], function(_, Backbone, Common, Device) {
|
||||
'use strict';
|
||||
|
||||
var DevicesCollection = Backbone.Collection.extend({
|
||||
model: Device,
|
||||
url: function () {
|
||||
return Common.getUrl({name: 'devices'});
|
||||
}
|
||||
});
|
||||
|
||||
return DevicesCollection;
|
||||
});
|
10
static/scripts/app/models/device.js
Normal file
10
static/scripts/app/models/device.js
Normal file
@ -0,0 +1,10 @@
|
||||
define([
|
||||
'underscore',
|
||||
'backbone'
|
||||
], function(_, Backbone) {
|
||||
'use strict';
|
||||
|
||||
var Device = Backbone.Model.extend({});
|
||||
|
||||
return Device;
|
||||
});
|
@ -29,6 +29,7 @@ define([
|
||||
'common/lib/:repo_id(/*path)': 'showCommonDir',
|
||||
'starred/': 'showStarredFile',
|
||||
'activities/': 'showActivities',
|
||||
'devices/': 'showDevices',
|
||||
// Default
|
||||
'*actions': 'showRepos'
|
||||
},
|
||||
@ -96,6 +97,12 @@ define([
|
||||
this.sideNavView.setCurTab('starred');
|
||||
},
|
||||
|
||||
showDevices: function() {
|
||||
this.switchCurrentView(this.myHomeView);
|
||||
this.myHomeView.showDevices();
|
||||
this.sideNavView.setCurTab('devices');
|
||||
},
|
||||
|
||||
showActivities: function() {
|
||||
this.switchCurrentView(this.myHomeView);
|
||||
this.myHomeView.showActivities();
|
||||
|
104
static/scripts/app/views/device.js
Normal file
104
static/scripts/app/views/device.js
Normal file
@ -0,0 +1,104 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common',
|
||||
], function($, _, Backbone, Common) {
|
||||
'use strict';
|
||||
|
||||
var DeviceView = Backbone.View.extend({
|
||||
tagName: 'tr',
|
||||
|
||||
template: _.template($('#device-item-tmpl').html()),
|
||||
|
||||
events: {
|
||||
'mouseenter': 'showAction',
|
||||
'mouseleave': 'hideAction',
|
||||
'click .unlink-device': 'unlinkDevice',
|
||||
'click .lib-num': 'showSyncedRepos'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var data = this.model.toJSON();
|
||||
|
||||
if (typeof(data['synced_repos']) == 'undefined') {
|
||||
data['synced_repos'] = new Array();
|
||||
}
|
||||
|
||||
if (data['synced_repos']) {
|
||||
data['synced_repos_length'] = data['synced_repos'].length;
|
||||
} else {
|
||||
data['synced_repos_length'] = 0;
|
||||
}
|
||||
|
||||
// convert to human readable time
|
||||
var now = new Date(),
|
||||
last_accessed = Common.getMomentWithLocale(data['last_accessed']);
|
||||
|
||||
data['time'] = last_accessed.format('LLLL');
|
||||
if (last_accessed - now > 0) {
|
||||
data['time_from_now'] = gettext("Just now");
|
||||
} else {
|
||||
data['time_from_now'] = last_accessed.fromNow();
|
||||
}
|
||||
|
||||
this.$el.html(this.template(data));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
showAction: function() {
|
||||
this.$el.addClass('hl');
|
||||
this.$el.find('.op-icon').removeClass('vh');
|
||||
},
|
||||
|
||||
hideAction: function() {
|
||||
this.$el.removeClass('hl');
|
||||
this.$el.find('.op-icon').addClass('vh');
|
||||
},
|
||||
|
||||
showSyncedRepos: function(e) {
|
||||
var $lib_num = $(e.currentTarget);
|
||||
var lib_list = $lib_num.next('.lib-list');
|
||||
var dir_icon = $lib_num.children('.dir-icon');
|
||||
|
||||
if (lib_list.length > 0) {
|
||||
lib_list.toggleClass('hide');
|
||||
if (lib_list.hasClass('hide')) {
|
||||
dir_icon.removeClass('icon-caret-up').addClass('icon-caret-down');
|
||||
} else {
|
||||
dir_icon.removeClass('icon-caret-down').addClass('icon-caret-up');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
unlinkDevice: function() {
|
||||
var _this = this,
|
||||
data = {
|
||||
'platform': this.model.get('platform'),
|
||||
'device_id': this.model.get('device_id')
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: Common.getUrl({name: 'devices'}),
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
beforeSend: Common.prepareCSRFToken,
|
||||
data: data,
|
||||
success: function() {
|
||||
_this.remove();
|
||||
Common.feedback(gettext("Success"), 'success');
|
||||
},
|
||||
error: function (xhr) {
|
||||
Common.ajaxErrorHandler(xhr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return DeviceView;
|
||||
});
|
58
static/scripts/app/views/devices.js
Normal file
58
static/scripts/app/views/devices.js
Normal file
@ -0,0 +1,58 @@
|
||||
define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common',
|
||||
'app/views/device',
|
||||
'app/collections/devices',
|
||||
], function($, _, Backbone, Common, Device, DevicesCollection) {
|
||||
'use strict';
|
||||
|
||||
var DevicesView = Backbone.View.extend({
|
||||
|
||||
el: $('#devices'),
|
||||
|
||||
initialize: function() {
|
||||
this.$table = this.$('table');
|
||||
this.$tableBody = this.$('tbody');
|
||||
this.$loadingTip = this.$('.loading-tip');
|
||||
this.$emptyTip = this.$('.empty-tips');
|
||||
|
||||
this.devices = new DevicesCollection();
|
||||
this.listenTo(this.devices, 'reset', this.reset);
|
||||
|
||||
},
|
||||
|
||||
addOne: function(device) {
|
||||
var view = new Device({model: device});
|
||||
this.$tableBody.append(view.render().el);
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.$tableBody.empty();
|
||||
this.$loadingTip.hide();
|
||||
this.devices.each(this.addOne, this);
|
||||
if (this.devices.length) {
|
||||
this.$emptyTip.hide();
|
||||
this.$table.show();
|
||||
} else {
|
||||
this.$emptyTip.show();
|
||||
this.$table.hide();
|
||||
}
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.$el.hide();
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.$el.show();
|
||||
this.$table.hide();
|
||||
this.$loadingTip.show();
|
||||
this.devices.fetch({reset: true});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return DevicesView;
|
||||
});
|
@ -6,9 +6,10 @@ define([
|
||||
'app/views/myhome-repos',
|
||||
'app/views/myhome-shared-repos',
|
||||
'app/views/starred-file',
|
||||
'app/views/devices',
|
||||
'app/views/activities'
|
||||
], function($, _, Backbone, Common, ReposView,
|
||||
SharedReposView, StarredFileView, ActivitiesView) {
|
||||
SharedReposView, StarredFileView, DevicesView, ActivitiesView) {
|
||||
'use strict';
|
||||
|
||||
var MyHomeView = Backbone.View.extend({
|
||||
@ -18,6 +19,7 @@ define([
|
||||
this.reposView = new ReposView();
|
||||
this.sharedReposView = new SharedReposView();
|
||||
this.starredFileView = new StarredFileView();
|
||||
this.devicesView = new DevicesView();
|
||||
this.activitiesView = new ActivitiesView();
|
||||
|
||||
this.dirView = options.dirView;
|
||||
@ -45,6 +47,12 @@ define([
|
||||
this.currentView = this.starredFileView;
|
||||
},
|
||||
|
||||
showDevices: function() {
|
||||
this.currentView.hide();
|
||||
this.devicesView.show();
|
||||
this.currentView = this.devicesView;
|
||||
},
|
||||
|
||||
showActivities: function() {
|
||||
this.currentView.hide();
|
||||
this.activitiesView.show();
|
||||
|
@ -41,6 +41,7 @@ require.config({
|
||||
simplemodal: 'lib/jquery.simplemodal',
|
||||
jstree: 'lib/jstree.1.0',
|
||||
select2: 'lib/select2-3.5.2',
|
||||
moment: 'lib/moment-with-locales',
|
||||
|
||||
underscore: 'lib/underscore',
|
||||
backbone: 'lib/backbone',
|
||||
@ -52,8 +53,9 @@ define([
|
||||
'jquery',
|
||||
'underscore',
|
||||
'text', // Workaround for r.js, otherwise text.js will not be included
|
||||
'pinyin-by-unicode'
|
||||
], function($, _, text, PinyinByUnicode) {
|
||||
'pinyin-by-unicode',
|
||||
'moment',
|
||||
], function($, _, text, PinyinByUnicode, Moment) {
|
||||
return {
|
||||
INFO_TIMEOUT: 10000, // 10 secs for info msg
|
||||
SUCCESS_TIMEOUT: 3000, // 3 secs for success msg
|
||||
@ -144,6 +146,7 @@ define([
|
||||
case 'set_notice_seen_by_id': return siteRoot + 'ajax/set_notice_seen_by_id/';
|
||||
case 'toggle_personal_modules': return siteRoot + 'ajax/toggle-personal-modules/';
|
||||
case 'starred_files': return siteRoot + 'api2/starredfiles/';
|
||||
case 'devices': return siteRoot + 'api2/devices/';
|
||||
case 'events': return siteRoot + 'api2/events/';
|
||||
case 'search_user': return siteRoot + 'api2/search-user/';
|
||||
case 'user_profile': return siteRoot + 'profile/' + options.username + '/';
|
||||
@ -191,6 +194,19 @@ define([
|
||||
'default' : 'file.png'
|
||||
},
|
||||
|
||||
getMomentWithLocale: function(time) {
|
||||
var language_code;
|
||||
if (app.pageOptions.language_code == 'en') {
|
||||
language_code = 'en-gb';
|
||||
} else if (app.pageOptions.language_code == 'es-ar' || app.pageOptions.language_code == 'es-mx') {
|
||||
language_code = 'es';
|
||||
} else {
|
||||
language_code = app.pageOptions.language_code;
|
||||
}
|
||||
|
||||
return Moment(time).locale(language_code);
|
||||
},
|
||||
|
||||
getFileIconUrl: function(filename, size) {
|
||||
if (size > 24) {
|
||||
size = 192;
|
||||
|
5746
static/scripts/lib/moment-with-locales.js
Normal file
5746
static/scripts/lib/moment-with-locales.js
Normal file
File diff suppressed because it is too large
Load Diff
40
tests/api/test_devices.py
Normal file
40
tests/api/test_devices.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from seahub.api2.models import TokenV2
|
||||
from seahub.test_utils import BaseTestCase, Fixtures
|
||||
|
||||
|
||||
class DevicesTest(BaseTestCase, Fixtures):
|
||||
def setUp(self):
|
||||
self.platform = 'android'
|
||||
self.device_id = '4a0d62c1f27b3b74'
|
||||
TokenV2.objects.get_or_create_token(self.user.username, self.platform,
|
||||
self.device_id, u'PLK-AL10', u'2.0.3', u'5.0.2', '192.168.1.208')
|
||||
|
||||
def tearDown(self):
|
||||
self.remove_repo()
|
||||
|
||||
def test_can_list(self):
|
||||
self.login_as(self.user)
|
||||
|
||||
resp = self.client.get(reverse('api2-devices'))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
json_resp = json.loads(resp.content)
|
||||
assert json_resp[0]['platform'] == self.platform
|
||||
assert json_resp[0]['device_id'] == self.device_id
|
||||
|
||||
def test_can_delete(self):
|
||||
self.login_as(self.user)
|
||||
data = 'platform=%s&device_id=%s' % (self.platform, self.device_id)
|
||||
resp = self.client.delete(reverse('api2-devices'), data, 'application/x-www-form-urlencoded')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(0, len(TokenV2.objects.all()))
|
||||
|
||||
def test_can_not_delete_with_invalid_args(self):
|
||||
self.login_as(self.user)
|
||||
|
||||
resp = self.client.delete(reverse('api2-devices'))
|
||||
self.assertEqual(400, resp.status_code)
|
Loading…
Reference in New Issue
Block a user