1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-10 11:22:09 +00:00

repo monitor (#5265)

Co-authored-by: lian <lian@seafile.com>
This commit is contained in:
lian 2023-01-16 17:56:05 +08:00 committed by GitHub
parent 1f25cf459c
commit 00b1007cae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 297 additions and 24 deletions

View File

@ -19,6 +19,7 @@ const MSG_TYPE_FILE_COMMENT = 'file_comment';
const MSG_TYPE_DRAFT_COMMENT = 'draft_comment'; const MSG_TYPE_DRAFT_COMMENT = 'draft_comment';
const MSG_TYPE_DRAFT_REVIEWER = 'draft_reviewer'; const MSG_TYPE_DRAFT_REVIEWER = 'draft_reviewer';
const MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted'; const MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted';
const MSG_TYPE_REPO_MONITOR = 'repo_monitor';
class NoticeItem extends React.Component { class NoticeItem extends React.Component {
@ -70,7 +71,7 @@ class NoticeItem extends React.Component {
notice = notice.replace('{share_from}', shareFrom); notice = notice.replace('{share_from}', shareFrom);
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
notice = Utils.HTMLescape(notice); notice = Utils.HTMLescape(notice);
// 3. add jump link // 3. add jump link
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`); notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
notice = notice.replace('{/tagA}', '</a>'); notice = notice.replace('{/tagA}', '</a>');
@ -98,13 +99,13 @@ class NoticeItem extends React.Component {
} else { } else {
notice = gettext('{share_from} has shared a folder named {repo_link} to group {group_link}.'); notice = gettext('{share_from} has shared a folder named {repo_link} to group {group_link}.');
} }
// 2. handle xss(cross-site scripting) // 2. handle xss(cross-site scripting)
notice = notice.replace('{share_from}', shareFrom); notice = notice.replace('{share_from}', shareFrom);
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
notice = notice.replace('{group_link}', `{tagB}${groupName}{/tagB}`); notice = notice.replace('{group_link}', `{tagB}${groupName}{/tagB}`);
notice = Utils.HTMLescape(notice); notice = Utils.HTMLescape(notice);
// 3. add jump link // 3. add jump link
notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`); notice = notice.replace('{tagA}', `<a href='${Utils.encodePath(repoUrl)}'>`);
notice = notice.replace('{/tagA}', '</a>'); notice = notice.replace('{/tagA}', '</a>');
@ -128,7 +129,7 @@ class NoticeItem extends React.Component {
notice = notice.replace('{user}', repoOwner); notice = notice.replace('{user}', repoOwner);
notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`);
notice = Utils.HTMLescape(notice); notice = Utils.HTMLescape(notice);
// 3. add jump link // 3. add jump link
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(repoUrl)}>`); notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(repoUrl)}>`);
notice = notice.replace('{/tagA}', '</a>'); notice = notice.replace('{/tagA}', '</a>');
@ -151,7 +152,7 @@ class NoticeItem extends React.Component {
notice = notice.replace('{upload_file_link}', `{tagA}${fileName}{/tagA}`); notice = notice.replace('{upload_file_link}', `{tagA}${fileName}{/tagA}`);
notice = notice.replace('{uploaded_link}', `{tagB}${folderName}{/tagB}`); notice = notice.replace('{uploaded_link}', `{tagB}${folderName}{/tagB}`);
notice = Utils.HTMLescape(notice); notice = Utils.HTMLescape(notice);
// 3. add jump link // 3. add jump link
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileLink)}>`); notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileLink)}>`);
notice = notice.replace('{/tagA}', '</a>'); notice = notice.replace('{/tagA}', '</a>');
@ -180,12 +181,12 @@ class NoticeItem extends React.Component {
// 1. handle translate // 1. handle translate
let notice = gettext('File {file_link} has a new comment form user {author}.'); let notice = gettext('File {file_link} has a new comment form user {author}.');
// 2. handle xss(cross-site scripting) // 2. handle xss(cross-site scripting)
notice = notice.replace('{file_link}', `{tagA}${fileName}{/tagA}`); notice = notice.replace('{file_link}', `{tagA}${fileName}{/tagA}`);
notice = notice.replace('{author}', author); notice = notice.replace('{author}', author);
notice = Utils.HTMLescape(notice); notice = Utils.HTMLescape(notice);
// 3. add jump link // 3. add jump link
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileUrl)}>`); notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(fileUrl)}>`);
notice = notice.replace('{/tagA}', '</a>'); notice = notice.replace('{/tagA}', '</a>');
@ -224,6 +225,72 @@ class NoticeItem extends React.Component {
return {avatar_url, notice}; return {avatar_url, notice};
} }
if (noticeType === MSG_TYPE_REPO_MONITOR) {
let avatar_url = detail.op_user_avatar_url;
let repoLink = siteRoot + 'library/' + detail.repo_id + '/' + detail.repo_name + '/';
let notice = '';
let op = '';
if (detail.obj_type == 'file') {
switch (detail.op_type) {
case 'create':
op = gettext('created file');
break;
case 'delete':
op = gettext('deleted file');
break;
case 'recover':
op = gettext('restored file');
break;
case 'rename':
op = gettext('renamed file');
break;
case 'move':
op = gettext('moved file');
break;
case 'edit':
op = gettext('updated file');
break;
}
} else { // dir
switch (detail.op_type) {
case 'create':
op = gettext('created folder');
break;
case 'delete':
op = gettext('deleted folder');
break;
case 'recover':
op = gettext('restored folder');
break;
case 'rename':
op = gettext('renamed folder');
break;
case 'move':
op = gettext('moved folder');
break;
}
}
// 1. handle translate
notice = gettext('{op_user} {op_type} {obj_name} in {repo_link}.');
let obj_name = Utils.getFileName(detail.obj_path_list[0]);
// 2. handle xss(cross-site scripting)
notice = notice.replace('{op_user}', `${detail.op_user_name}`);
notice = notice.replace('{op_type}', `${op}`);
notice = notice.replace('{obj_name}', `${obj_name}`);
notice = notice.replace('{repo_link}', `{tagA}${detail.repo_name}{/tagA}`);
notice = Utils.HTMLescape(notice);
// 3. add jump link
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(repoLink)}>`);
notice = notice.replace('{/tagA}', '</a>');
return {avatar_url, notice};
}
// if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) { // if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) {
// } // }

View File

@ -17,6 +17,7 @@ class Repo {
this.modifier_name = object.modifier_name; this.modifier_name = object.modifier_name;
this.type = object.type; this.type = object.type;
this.starred = object.starred; this.starred = object.starred;
this.monitored = object.monitored;
this.status = object.status; this.status = object.status;
this.storage_name = object.storage_name; this.storage_name = object.storage_name;
if (object.is_admin != undefined) { if (object.is_admin != undefined) {

View File

@ -40,6 +40,7 @@ class MylibRepoListItem extends React.Component {
this.state = { this.state = {
isOpIconShow: false, isOpIconShow: false,
isStarred: this.props.repo.starred, isStarred: this.props.repo.starred,
isMonitored: this.props.repo.monitored,
isRenaming: false, isRenaming: false,
isShareDialogShow: false, isShareDialogShow: false,
isDeleteDialogShow: false, isDeleteDialogShow: false,
@ -165,6 +166,32 @@ class MylibRepoListItem extends React.Component {
} }
} }
onToggleMonitorRepo = (e) => {
e.preventDefault();
const repoName = this.props.repo.repo_name;
if (this.state.isMonitored) {
seafileAPI.unMonitorRepo(this.props.repo.repo_id).then(() => {
this.setState({isMonitored: !this.state.isMonitored});
const msg = gettext('Successfully unmonitored {library_name_placeholder}.')
.replace('{library_name_placeholder}', repoName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
seafileAPI.monitorRepo(this.props.repo.repo_id).then(() => {
this.setState({isMonitored: !this.state.isMonitored});
const msg = gettext('Successfully monitored {library_name_placeholder}.')
.replace('{library_name_placeholder}', repoName);
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
}
onShareToggle = (e) => { onShareToggle = (e) => {
// when close share dialog after send share link email, // when close share dialog after send share link email,
// there is no event // there is no event
@ -297,6 +324,13 @@ class MylibRepoListItem extends React.Component {
<i className={`fa-star ${this.state.isStarred ? 'fas' : 'far star-empty'}`}></i> <i className={`fa-star ${this.state.isStarred ? 'fas' : 'far star-empty'}`}></i>
</a> </a>
</td> </td>
<td className="text-center">
<a href="#" role="button" aria-label={this.state.isMonitored ? gettext('unMonitor') : gettext('Monitor')} onClick={this.onToggleMonitorRepo}>
<i className={`fa-star ${this.state.isMonitored ? 'fas' : 'far star-empty'}`}></i>
</a>
</td>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td> <td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td> <td>
{this.state.isRenaming && ( {this.state.isRenaming && (

View File

@ -85,8 +85,9 @@ class MylibRepoListView extends React.Component {
<tr> <tr>
<th width="4%"></th> <th width="4%"></th>
<th width="4%"><span className="sr-only">{gettext('Library Type')}</span></th> <th width="4%"><span className="sr-only">{gettext('Library Type')}</span></th>
<th width="4%"><span className="sr-only">{gettext('Library Type')}</span></th>
<th width={showStorageBackend ? '33%' : '38%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {this.props.sortBy === 'name' && sortIcon}</a></th> <th width={showStorageBackend ? '33%' : '38%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {this.props.sortBy === 'name' && sortIcon}</a></th>
<th width="14%"><span className="sr-only">{gettext('Actions')}</span></th> <th width="10%"><span className="sr-only">{gettext('Actions')}</span></th>
<th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {this.props.sortBy === 'size' && sortIcon}</a></th> <th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {this.props.sortBy === 'size' && sortIcon}</a></th>
{showStorageBackend ? <th width="15%">{gettext('Storage Backend')}</th> : null} {showStorageBackend ? <th width="15%">{gettext('Storage Backend')}</th> : null}
<th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {this.props.sortBy === 'time' && sortIcon}</a></th> <th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {this.props.sortBy === 'time' && sortIcon}</a></th>

View File

@ -0,0 +1,111 @@
# Copyright (c) 2012-2018 Seafile Ltd.
import logging
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from seaserv import seafile_api
from seahub.api2.utils import api_error
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.utils.repo import is_repo_owner
from seahub.base.models import UserMonitoredRepos
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
logger = logging.getLogger(__name__)
class MonitoredRepos(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def post(self, request):
""" Monitor a repo.
Permission checking:
1. Only repo owner can perform this action.
"""
# argument check
repo_id = request.data.get('repo_id', None)
if not repo_id:
error_msg = 'repo_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
email = request.user.username
if not is_repo_owner(request, repo_id, email):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# monitor a repo
monitored_repos = UserMonitoredRepos.objects.filter(email=email, repo_id=repo_id)
if monitored_repos:
error_msg = 'Library {} has been monitored.'.format(repo_id)
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
try:
monitored_repo = UserMonitoredRepos.objects.create(email=email,
repo_id=repo_id)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
# get info of new monitored repo
item_info = {}
item_info['user_email'] = email
item_info['user_name'] = email2nickname(email)
item_info['user_contact_email'] = email2contact_email(email)
item_info['repo_id'] = monitored_repo.repo_id
return Response(item_info)
class MonitoredRepo(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def delete(self, request, repo_id):
""" Unmonitored repo.
Permission checking:
1. Only repo owner can perform this action.
"""
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
email = request.user.username
if not is_repo_owner(request, repo_id, email):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# unmonitor repo
try:
UserMonitoredRepos.objects.filter(email=email, repo_id=repo_id).delete()
except Exception 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})

View File

@ -14,9 +14,8 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.notifications.models import UserNotification from seahub.notifications.models import UserNotification
from seahub.notifications.models import get_cache_key_of_unseen_notifications from seahub.notifications.models import get_cache_key_of_unseen_notifications
from seahub.notifications.views import add_notice_from_info
from seahub.notifications.utils import update_notice_detail from seahub.notifications.utils import update_notice_detail
from seahub.api2.utils import api_error, to_python_boolean from seahub.api2.utils import api_error
from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils.timeutils import datetime_to_isoformat_timestr
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -117,7 +116,6 @@ class NotificationsView(APIView):
return Response({'success': True}) return Response({'success': True})
class NotificationView(APIView): class NotificationView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)

View File

@ -14,7 +14,7 @@ from seahub.api2.utils import api_error
from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
from seahub.base.models import UserStarredFiles from seahub.base.models import UserStarredFiles, UserMonitoredRepos
from seahub.base.templatetags.seahub_tags import email2nickname, \ from seahub.base.templatetags.seahub_tags import email2nickname, \
email2contact_email email2contact_email
from seahub.signals import repo_deleted from seahub.signals import repo_deleted
@ -79,6 +79,13 @@ class ReposView(APIView):
logger.error(e) logger.error(e)
starred_repo_id_list = [] starred_repo_id_list = []
try:
monitored_repos = UserMonitoredRepos.objects.filter(email=email)
monitored_repo_id_list = [item.repo_id for item in monitored_repos]
except Exception as e:
logger.error(e)
monitored_repo_id_list = []
repo_info_list = [] repo_info_list = []
if filter_by['mine']: if filter_by['mine']:
@ -120,6 +127,7 @@ class ReposView(APIView):
"encrypted": r.encrypted, "encrypted": r.encrypted,
"permission": 'rw', # Always have read-write permission to owned repo "permission": 'rw', # Always have read-write permission to owned repo
"starred": r.repo_id in starred_repo_id_list, "starred": r.repo_id in starred_repo_id_list,
"monitored": r.repo_id in monitored_repo_id_list,
"status": normalize_repo_status_code(r.status), "status": normalize_repo_status_code(r.status),
"salt": r.salt if r.enc_version >= 3 else '', "salt": r.salt if r.enc_version >= 3 else '',
} }

View File

@ -1,6 +1,5 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import os import os
import datetime
import logging import logging
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
@ -10,7 +9,7 @@ from pysearpc import SearpcError
from seaserv import seafile_api from seaserv import seafile_api
from seahub.auth.signals import user_logged_in from seahub.auth.signals import user_logged_in
from seahub.utils import calc_file_path_hash, within_time_range, \ from seahub.utils import within_time_range, \
normalize_file_path, normalize_dir_path normalize_file_path, normalize_dir_path
from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.tags.models import FileUUIDMap from seahub.tags.models import FileUUIDMap
@ -37,6 +36,7 @@ class TimestampedModel(models.Model):
# default ordering for most models. # default ordering for most models.
ordering = ['-created_at', '-updated_at'] ordering = ['-created_at', '-updated_at']
class FileCommentManager(models.Manager): class FileCommentManager(models.Manager):
def add(self, repo_id, parent_path, item_name, author, comment, detail=''): def add(self, repo_id, parent_path, item_name, author, comment, detail=''):
fileuuidmap = FileUUIDMap.objects.get_or_create_fileuuidmap(repo_id, fileuuidmap = FileUUIDMap.objects.get_or_create_fileuuidmap(repo_id,
@ -67,7 +67,7 @@ class FileCommentManager(models.Manager):
def get_by_parent_path(self, repo_id, parent_path): def get_by_parent_path(self, repo_id, parent_path):
uuids = FileUUIDMap.objects.get_fileuuidmaps_by_parent_path(repo_id, uuids = FileUUIDMap.objects.get_fileuuidmaps_by_parent_path(repo_id,
parent_path) parent_path)
objs = super(FileCommentManager, self).filter(uuid__in=uuids) objs = super(FileCommentManager, self).filter(uuid__in=uuids)
return objs return objs
@ -104,7 +104,7 @@ class FileComment(models.Model):
} }
########## starred files # starred files
class StarredFile(object): class StarredFile(object):
def format_path(self): def format_path(self):
if self.path == "/": if self.path == "/":
@ -129,6 +129,7 @@ class StarredFile(object):
if not is_dir: if not is_dir:
self.name = path.split('/')[-1] self.name = path.split('/')[-1]
class UserStarredFilesManager(models.Manager): class UserStarredFilesManager(models.Manager):
def get_starred_repos_by_user(self, email): def get_starred_repos_by_user(self, email):
@ -139,23 +140,26 @@ class UserStarredFilesManager(models.Manager):
def get_starred_item(self, email, repo_id, path): def get_starred_item(self, email, repo_id, path):
path_list = [normalize_file_path(path), normalize_dir_path(path)] path_list = [normalize_file_path(path), normalize_dir_path(path)]
starred_items = UserStarredFiles.objects.filter(email=email, starred_items = UserStarredFiles.objects.filter(email=email, repo_id=repo_id) \
repo_id=repo_id).filter(Q(path__in=path_list)) .filter(Q(path__in=path_list))
return starred_items[0] if len(starred_items) > 0 else None return starred_items[0] if len(starred_items) > 0 else None
def add_starred_item(self, email, repo_id, path, is_dir, org_id=-1): def add_starred_item(self, email, repo_id, path, is_dir, org_id=-1):
starred_item = UserStarredFiles.objects.create(email=email, starred_item = UserStarredFiles.objects.create(email=email,
repo_id=repo_id, path=path, is_dir=is_dir, org_id=org_id) repo_id=repo_id,
path=path,
is_dir=is_dir,
org_id=org_id)
return starred_item return starred_item
def delete_starred_item(self, email, repo_id, path): def delete_starred_item(self, email, repo_id, path):
path_list = [normalize_file_path(path), normalize_dir_path(path)] path_list = [normalize_file_path(path), normalize_dir_path(path)]
starred_items = UserStarredFiles.objects.filter(email=email, starred_items = UserStarredFiles.objects.filter(email=email, repo_id=repo_id) \
repo_id=repo_id).filter(Q(path__in=path_list)) .filter(Q(path__in=path_list))
for item in starred_items: for item in starred_items:
item.delete() item.delete()
@ -201,8 +205,9 @@ class UserStarredFilesManager(models.Manager):
sfile.delete() sfile.delete()
continue continue
# TODO: remove ``size`` from StarredFile
f = StarredFile(sfile.org_id, repo, file_id, sfile.path, f = StarredFile(sfile.org_id, repo, file_id, sfile.path,
sfile.is_dir, 0) # TODO: remove ``size`` from StarredFile sfile.is_dir, 0)
ret.append(f) ret.append(f)
'''Calculate files last modification time''' '''Calculate files last modification time'''
@ -227,6 +232,7 @@ class UserStarredFilesManager(models.Manager):
return ret return ret
class UserStarredFiles(models.Model): class UserStarredFiles(models.Model):
"""Starred files are marked by users to get quick access to it on user """Starred files are marked by users to get quick access to it on user
home page. home page.
@ -240,7 +246,8 @@ class UserStarredFiles(models.Model):
objects = UserStarredFilesManager() objects = UserStarredFilesManager()
########## misc
# misc
class UserLastLoginManager(models.Manager): class UserLastLoginManager(models.Manager):
def get_by_username(self, username): def get_by_username(self, username):
"""Return last login record for a user, delete duplicates if there are """Return last login record for a user, delete duplicates if there are
@ -258,11 +265,13 @@ class UserLastLoginManager(models.Manager):
logger.warn('Delete duplicate user last login record: %s' % username) logger.warn('Delete duplicate user last login record: %s' % username)
return ret return ret
class UserLastLogin(models.Model): class UserLastLogin(models.Model):
username = models.CharField(max_length=255, db_index=True) username = models.CharField(max_length=255, db_index=True)
last_login = models.DateTimeField(default=timezone.now) last_login = models.DateTimeField(default=timezone.now)
objects = UserLastLoginManager() objects = UserLastLoginManager()
def update_last_login(sender, user, **kwargs): def update_last_login(sender, user, **kwargs):
""" """
A signal receiver which updates the last_login date for A signal receiver which updates the last_login date for
@ -273,14 +282,18 @@ def update_last_login(sender, user, **kwargs):
user_last_login = UserLastLogin(username=user.username) user_last_login = UserLastLogin(username=user.username)
user_last_login.last_login = timezone.now() user_last_login.last_login = timezone.now()
user_last_login.save() user_last_login.save()
user_logged_in.connect(update_last_login) user_logged_in.connect(update_last_login)
class CommandsLastCheck(models.Model): class CommandsLastCheck(models.Model):
"""Record last check time for Django/custom commands. """Record last check time for Django/custom commands.
""" """
command_type = models.CharField(max_length=100) command_type = models.CharField(max_length=100)
last_check = models.DateTimeField() last_check = models.DateTimeField()
class DeviceToken(models.Model): class DeviceToken(models.Model):
""" """
The iOS device token model. The iOS device token model.
@ -297,8 +310,10 @@ class DeviceToken(models.Model):
def __unicode__(self): def __unicode__(self):
return "/".join(self.user, self.token) return "/".join(self.user, self.token)
_CLIENT_LOGIN_TOKEN_EXPIRATION_SECONDS = 30 _CLIENT_LOGIN_TOKEN_EXPIRATION_SECONDS = 30
class ClientLoginTokenManager(models.Manager): class ClientLoginTokenManager(models.Manager):
def get_username(self, tokenstr): def get_username(self, tokenstr):
try: try:
@ -312,6 +327,7 @@ class ClientLoginTokenManager(models.Manager):
return None return None
return username return username
class ClientLoginToken(models.Model): class ClientLoginToken(models.Model):
# TODO: update sql/mysql.sql and sql/sqlite3.sql # TODO: update sql/mysql.sql and sql/sqlite3.sql
token = models.CharField(max_length=32, primary_key=True) token = models.CharField(max_length=32, primary_key=True)
@ -349,3 +365,11 @@ class RepoSecretKey(models.Model):
secret_key = models.CharField(max_length=44) secret_key = models.CharField(max_length=44)
objects = RepoSecretKeyManager() objects = RepoSecretKeyManager()
class UserMonitoredRepos(models.Model):
"""
"""
email = models.EmailField(db_index=True)
repo_id = models.CharField(max_length=36, db_index=True)
timestamp = models.DateTimeField(default=timezone.now)

View File

@ -74,6 +74,7 @@ MSG_TYPE_DRAFT_COMMENT = 'draft_comment'
MSG_TYPE_DRAFT_REVIEWER = 'draft_reviewer' MSG_TYPE_DRAFT_REVIEWER = 'draft_reviewer'
MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted' MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted'
MSG_TYPE_REPO_TRANSFER = 'repo_transfer' MSG_TYPE_REPO_TRANSFER = 'repo_transfer'
MSG_TYPE_REPO_MINOTOR = 'repo_monitor'
USER_NOTIFICATION_COUNT_CACHE_PREFIX = 'USER_NOTIFICATION_COUNT_' USER_NOTIFICATION_COUNT_CACHE_PREFIX = 'USER_NOTIFICATION_COUNT_'
@ -403,6 +404,9 @@ class UserNotification(models.Model):
def is_repo_transfer_msg(self): def is_repo_transfer_msg(self):
return self.msg_type == MSG_TYPE_REPO_TRANSFER return self.msg_type == MSG_TYPE_REPO_TRANSFER
def is_repo_monitor_msg(self):
return self.msg_type == MSG_TYPE_REPO_MINOTOR
def user_message_detail_to_dict(self): def user_message_detail_to_dict(self):
"""Parse user message detail, returns dict contains ``message`` and """Parse user message detail, returns dict contains ``message`` and
``msg_from``. ``msg_from``.

View File

@ -3,7 +3,6 @@ import os
import json import json
import logging import logging
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse
from seaserv import ccnet_api, seafile_api from seaserv import ccnet_api, seafile_api
from seahub.notifications.models import Notification from seahub.notifications.models import Notification
@ -227,4 +226,25 @@ def update_notice_detail(request, notices):
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
elif notice.is_repo_monitor_msg():
try:
d = json.loads(notice.detail)
repo_id = d['repo_id']
if repo_id in repo_dict:
repo = repo_dict[repo_id]
else:
repo = seafile_api.get_repo(repo_id)
repo_dict[repo_id] = repo
op_user_email = d.pop('op_user')
url, is_default, date_uploaded = api_avatar_url(op_user_email, 32)
d['op_user_avatar_url'] = url
d['op_user_email'] = op_user_email
d['op_user_name'] = email2nickname(op_user_email)
d['op_user_contact_email'] = email2contact_email(op_user_email)
notice.detail = d
except Exception as e:
logger.error(e)
return notices return notices

View File

@ -94,6 +94,7 @@ from seahub.api2.endpoints.tag_filter_file import TaggedFilesView
from seahub.api2.endpoints.related_files import RelatedFilesView, RelatedFileView from seahub.api2.endpoints.related_files import RelatedFilesView, RelatedFileView
from seahub.api2.endpoints.webdav_secret import WebdavSecretView from seahub.api2.endpoints.webdav_secret import WebdavSecretView
from seahub.api2.endpoints.starred_items import StarredItems from seahub.api2.endpoints.starred_items import StarredItems
from seahub.api2.endpoints.monitored_repos import MonitoredRepos, MonitoredRepo
from seahub.api2.endpoints.markdown_lint import MarkdownLintView from seahub.api2.endpoints.markdown_lint import MarkdownLintView
from seahub.api2.endpoints.public_repos_search import PublishedRepoSearchView from seahub.api2.endpoints.public_repos_search import PublishedRepoSearchView
from seahub.api2.endpoints.recent_added_files import RecentAddedFilesView from seahub.api2.endpoints.recent_added_files import RecentAddedFilesView
@ -467,6 +468,10 @@ urlpatterns = [
## user::starred-item ## user::starred-item
url(r'^api/v2.1/starred-items/$', StarredItems.as_view(), name='api-v2.1-starred-items'), url(r'^api/v2.1/starred-items/$', StarredItems.as_view(), name='api-v2.1-starred-items'),
## user::monitored-repos
url(r'^api/v2.1/monitored-repos/$', MonitoredRepos.as_view(), name='api-v2.1-monitored-repos'),
url(r'^api/v2.1/monitored-repos/(?P<repo_id>[-0-9a-f]{36})/$', MonitoredRepo.as_view(), name='api-v2.1-monitored-repo'),
## user::wiki ## user::wiki
url(r'^api/v2.1/wikis/$', WikisView.as_view(), name='api-v2.1-wikis'), url(r'^api/v2.1/wikis/$', WikisView.as_view(), name='api-v2.1-wikis'),
url(r'^api/v2.1/wikis/(?P<slug>[^/]+)/$', WikiView.as_view(), name='api-v2.1-wiki'), url(r'^api/v2.1/wikis/(?P<slug>[^/]+)/$', WikiView.as_view(), name='api-v2.1-wiki'),