diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index ffd44e4b6e..1490a28119 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -73,7 +73,12 @@ module.exports = { require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/file-history.js", - ] + ], + drafts: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/drafts.js", + ] }, output: { diff --git a/frontend/src/components/constance.js b/frontend/src/components/constance.js index c8503ca2e4..5284c45fd6 100644 --- a/frontend/src/components/constance.js +++ b/frontend/src/components/constance.js @@ -9,6 +9,7 @@ export const siteTitle = window.app.config.siteTitle; export const logoWidth = window.app.config.logoWidth; export const logoHeight = window.app.config.logoHeight; export const isPro = window.app.config.isPro === "True"; +export const lang = window.app.config.lang; // wiki export const slug = window.wiki ? window.wiki.config.slug : ''; diff --git a/frontend/src/components/list-view/list-item.js b/frontend/src/components/list-view/list-item.js new file mode 100644 index 0000000000..1b856d7598 --- /dev/null +++ b/frontend/src/components/list-view/list-item.js @@ -0,0 +1,82 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { siteRoot, lang } from '../constance'; +import NodeMenuControl from '../menu-component/node-menu-control'; +import moment from 'moment'; + +moment.locale(lang); +const propTypes = { + isItemFreezed: PropTypes.bool.isRequired, + onMenuToggleClick: PropTypes.func.isRequired, +} +class ListItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isMenuControlShow: false, + highlight: '', + }; + } + + onMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isMenuControlShow: true, + highlight: 'tr-highlight' + }); + } + } + + onMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + isMenuControlShow: false, + highlight: '' + }); + } + } + + onMenuToggleClick = (e) => { + e.nativeEvent.stopImmediatePropagation(); + let draft = this.props.draft; + this.props.onMenuToggleClick(e, draft); + } + + onDraftEditClick = () => { + let draft = this.props.draft; + let filePath = draft.draft_file_path; + let repoID = draft.draft_repo_id; + window.location.href= siteRoot + 'lib/' + repoID + '/file' + filePath + '?mode=edit'; + } + + getFileName(filePath) { + let lastIndex = filePath.lastIndexOf("/"); + return filePath.slice(lastIndex+1); + } + + render() { + let draft = this.props.draft; + let fileName = this.getFileName(draft.draft_file_path); + let localTime = moment.utc(draft.updated_at).toDate(); + localTime = moment(localTime).fromNow(); + return ( + + + {fileName} + {draft.owner} + {localTime} + + + + + ); + } +} + +ListItem.propTypes = propTypes; + +export default ListItem; diff --git a/frontend/src/components/list-view/list-menu.js b/frontend/src/components/list-view/list-menu.js new file mode 100644 index 0000000000..093d0b85ac --- /dev/null +++ b/frontend/src/components/list-view/list-menu.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../constance'; + +const propTypes = { + isMenuShow: PropTypes.bool.isRequired, + menuPosition: PropTypes.object.isRequired, + onDeleteHandler: PropTypes.func.isRequired, + onPublishHandler: PropTypes.func.isRequired +}; + +class ListMenu extends React.Component { + + render() { + let style = ''; + let {isMenuShow, menuPosition} = this.props; + if (isMenuShow) { + style = {position: 'fixed', top: menuPosition.top, left: menuPosition.left, display: 'block'} + } + return ( +
+ +
+ ); + } +} + +ListMenu.propTypes = propTypes; + +export default ListMenu; diff --git a/frontend/src/components/list-view/list-view.js b/frontend/src/components/list-view/list-view.js new file mode 100644 index 0000000000..a4e177916a --- /dev/null +++ b/frontend/src/components/list-view/list-view.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../constance'; +import ListItem from './list-item'; + +const propTypes = { + isItemFreezed: PropTypes.bool.isRequired, + draftList: PropTypes.array.isRequired, + onMenuToggleClick: PropTypes.func.isRequired, +}; + +class ListView extends React.Component { + + render() { + let drafts = this.props.draftList; + return ( + + + + + + + + + + + + { drafts && drafts.map((draft) => { + return ( + + ); + })} + +
{/*img*/}{gettext('Name')}{gettext('Owner')}{gettext('Update time')}
+ ); + } +} + +ListView.propTypes = propTypes; + +export default ListView; diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js index 1c2f79d849..3a8844e4a7 100644 --- a/frontend/src/components/main-side-nav.js +++ b/frontend/src/components/main-side-nav.js @@ -114,6 +114,11 @@ class MainSideNav extends React.Component { Share Admin {this.renderSharedAdmin()} +
  • + + Drafts + +
  • diff --git a/frontend/src/css/common.css b/frontend/src/css/common.css new file mode 100644 index 0000000000..f406855083 --- /dev/null +++ b/frontend/src/css/common.css @@ -0,0 +1,100 @@ +.panel-heading { + position: relative; + padding: .5rem 1rem; + width: 100%; + border-bottom: 1px solid #e8e8e8; + font-size: 1rem; + font-weight: normal; + line-height: 1.5; + height: 36px; + text-align: center; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.a-simulate { + color: #eb8205 !important; + text-decoration: none; + font-weight: normal; + cursor: pointer; +} + +.a-simulate:hover { + text-decoration: underline; +} + +.flex-right { + justify-content: flex-end; +} +/* begin main table list style */ + +.table-container { + flex: 1; + padding: 10px 1rem; + overflow: auto; +} + +.table-container table { + width: 100%; +} + +.table-container table th { + text-align: left; + font-weight: normal; + color: #9c9c9c; +} + +.table-container table td { + color: #333; + font-size: 14px; + word-break: break-all; +} + +.table-container table th, .table-container table td { + padding-top: 5px; + padding-bottom: 5px; + border-bottom: 1px solid #eee; +} + +.table-container table tr img { + display: block; + width: 24px; + height: 24px; +} +/* specific handler */ +.table-container table .menu-toggle { + text-align: center; +} + +.tr-highlight { + background-color: #f8f8f8; +} + +/* end main list style */ + +/* begin dropdown-menu style */ +.dropdown-menu { + min-width: 8rem; +} +/* end dropdown-menu style */ + +/* begin tip */ +.empty-tip { + padding: 30px 40px; + background-color: #FAFAFA; + border: solid 1px #DDD; + border-radius: 3px; + box-shadow: inset 0 0 8px #EEE; + margin-top: 5.5em; +} + +.empty-tip h2 { + text-align: center; +} +/* end tip */ diff --git a/frontend/src/dashboard.js b/frontend/src/dashboard.js index e9c546c906..2daac88a96 100644 --- a/frontend/src/dashboard.js +++ b/frontend/src/dashboard.js @@ -8,7 +8,6 @@ import Notification from './components/notification'; import { SeafileAPI } from 'seafile-js'; import cookie from 'react-cookies'; - import 'seafile-ui'; import './assets/css/fa-solid.css'; import './assets/css/fa-regular.css'; diff --git a/frontend/src/drafts.js b/frontend/src/drafts.js new file mode 100644 index 0000000000..02fdea4dc5 --- /dev/null +++ b/frontend/src/drafts.js @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import editUtilties from './utils/editor-utilties'; +import SidePanel from './pages/drafts/side-panel'; +import MainPanel from './pages/drafts/main-panel'; + +import 'seafile-ui'; +import './assets/css/fa-solid.css'; +import './assets/css/fa-regular.css'; +import './assets/css/fontawesome.css'; +import './css/layout.css'; +import './css/common.css'; + +class Drafts extends Component { + + constructor(props) { + super(props); + this.state = { + draftList: [], + isLoadingDraft: true, + }; + } + + componentDidMount() { + this.initDraftList(); + } + + initDraftList() { + editUtilties.listDrafts().then(res => { + this.setState({ + draftList: res.data.data, + isLoadingDraft: false, + }); + }); + } + + deleteDraft = (draft) => { + editUtilties.deleteDraft(draft.id).then(res => { + this.initDraftList(); + }) + } + + publishDraft = (draft) => { + editUtilties.publishDraft(draft.id).then(res => { + this.initDraftList(); + }) + } + + render() { + return ( +
    + + +
    + ); + } +} + +ReactDOM.render( + , + document.getElementById('wrapper') +); diff --git a/frontend/src/pages/drafts/main-panel.js b/frontend/src/pages/drafts/main-panel.js new file mode 100644 index 0000000000..ba626cd46f --- /dev/null +++ b/frontend/src/pages/drafts/main-panel.js @@ -0,0 +1,113 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../components/constance'; +import Loading from '../../components/loading'; +import Account from '../../components/account'; +import { seafileAPI } from '../../utils/editor-utilties'; +import Notification from '../../components/notification'; +import ListView from '../../components/list-view/list-view'; +import ListMenu from '../../components/list-view/list-menu'; + +const propTypes = { + draftList: PropTypes.array.isRequired, +}; + +class MainPanel extends React.Component { + + constructor(props) { + super(props); + this.state = { + isMenuShow: false, + menuPosition: {top:'', left: ''}, + currentDraft: null, + isItemFreezed: false, + }; + } + + componentDidMount() { + document.addEventListener('click', this.onHideContextMenu); + } + + componentWillUnmount() { + document.removeEventListener('click', this.onHideContextMenu); + } + + onMenuToggleClick = (e, draft) => { + if (this.state.isMenuShow) { + this.onHideContextMenu(); + } else { + this.onShowContextMenu(e, draft); + } + } + + onShowContextMenu = (e, draft) => { + let left = e.clientX - 8*16; + let top = e.clientY + 10; + let position = {top: top, left: left}; + this.setState({ + isMenuShow: true, + menuPosition: position, + currentDraft: draft, + isItemFreezed: true + }); + } + + onHideContextMenu = () => { + this.setState({ + isMenuShow: false, + currentDraft: null, + isItemFreezed: false + }); + } + + onPublishHandler = () => { + this.props.publishDraft(this.state.currentDraft); + } + + onDeleteHandler = () => { + this.props.deleteDraft(this.state.currentDraft); + } + + render() { + return ( +
    +
    + + +
    +
    +
    {gettext('Drafts')}
    + {this.props.isLoadingDraft ? + : +
    + {this.props.draftList.length ? + : +
    +

    {gettext('There is no draft file existing')}

    +
    + } +
    + } + { + this.state.isMenuShow && + + } +
    +
    + ); + } +} + +MainPanel.propTypes = propTypes; + +export default MainPanel; diff --git a/frontend/src/pages/drafts/side-panel.js b/frontend/src/pages/drafts/side-panel.js new file mode 100644 index 0000000000..fe702704a7 --- /dev/null +++ b/frontend/src/pages/drafts/side-panel.js @@ -0,0 +1,24 @@ +import React from 'react'; +import Logo from '../../components/logo'; +import MainSideNav from '../../components/main-side-nav'; +import SideNavFooter from '../../components/side-nav-footer'; + + +class SidePanel extends React.Component { + + render() { + return ( +
    +
    +
    + +
    +
    + +
    +
    + ); + } +} + +export default SidePanel; diff --git a/frontend/src/utils/editor-utilties.js b/frontend/src/utils/editor-utilties.js index ef3ab92fcf..15467b3e16 100644 --- a/frontend/src/utils/editor-utilties.js +++ b/frontend/src/utils/editor-utilties.js @@ -97,9 +97,21 @@ class EditorUtilities { revertFile(filePath, commitID) { return seafileAPI.revertFile(historyRepoID, filePath, commitID); } + + listDrafts() { + return seafileAPI.listDrafts(); + } + + deleteDraft(id) { + return seafileAPI.deleteDraft(id); + } + + publishDraft(id) { + return seafileAPI.publishDraft(id); + } } const editorUtilities = new EditorUtilities(); export default editorUtilities; -export { seafileAPI }; +export { seafileAPI }; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index b20149bd04..403b33565c 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -34,6 +34,7 @@ .sf2-icon-x3:before { content:"\e035"; } .sf2-icon-grid-view:before { content:"\e025"; } .sf2-icon-list-view:before { content:"\e026"; } +.sf2-icon-edit:before { content:"\e018"; } /* common class and element style*/ a { color:#eb8205; } diff --git a/seahub/api2/endpoints/drafts.py b/seahub/api2/endpoints/drafts.py new file mode 100644 index 0000000000..01ab0a044d --- /dev/null +++ b/seahub/api2/endpoints/drafts.py @@ -0,0 +1,122 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import json +import logging + +from rest_framework import status +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 seaserv import seafile_api, edit_repo +from pysearpc import SearpcError +from django.core.urlresolvers import reverse +from django.db import IntegrityError +from django.db.models import Count +from django.http import HttpResponse +from django.utils.translation import ugettext as _ + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.endpoints.utils import add_org_context +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.constants import PERMISSION_READ_WRITE +from seahub.drafts.models import Draft, DraftFileExist, DraftFileConflict +from seahub.views import check_folder_permission + +logger = logging.getLogger(__name__) + + +class DraftsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, format=None): + """List all user drafts. + """ + username = request.user.username + data = [x.to_dict() for x in Draft.objects.filter(username=username)] + + return Response({'data': data}) + + @add_org_context + def post(self, request, org_id, format=None): + """Create a file draft. + """ + repo_id = request.POST.get('repo_id', '') + file_path = request.POST.get('file_path', '') + + 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) + + file_id = seafile_api.get_file_id_by_path(repo.id, file_path) + if not file_id: + return api_error(status.HTTP_404_NOT_FOUND, + "File %s not found" % file_path) + + # perm check + perm = check_folder_permission(request, repo.id, file_path) + if perm != PERMISSION_READ_WRITE: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + username = request.user.username + + try: + d = Draft.objects.add(username, repo, file_path, file_id, org_id=org_id) + + return Response(d.to_dict()) + except (DraftFileExist, IntegrityError): + return api_error(status.HTTP_409_CONFLICT, 'Draft already exists.') + + +class DraftView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def put(self, request, pk, format=None): + """Publish a draft. + """ + op = request.data.get('operation', '') + if op != 'publish': + return api_error(status.HTTP_400_BAD_REQUEST, + 'Operation %s invalid.') + + try: + d = Draft.objects.get(pk=pk) + except Draft.DoesNotExist: + return api_error(status.HTTP_404_NOT_FOUND, + 'Draft %s not found.' % pk) + + # perm check + if d.username != request.user.username: + return api_error(status.HTTP_403_FORBIDDEN, + 'Permission denied.') + + try: + d.publish() + return Response(status.HTTP_200_OK) + except (DraftFileConflict, IntegrityError): + return api_error(status.HTTP_409_CONFLICT, + 'There is a conflict between the draft and the original file') + + def delete(self, request, pk, format=None): + """Delete a draft. + """ + try: + d = Draft.objects.get(pk=pk) + except Draft.DoesNotExist: + return api_error(status.HTTP_404_NOT_FOUND, + 'Draft %s not found.' % pk) + + # perm check + if d.username != request.user.username: + return api_error(status.HTTP_403_FORBIDDEN, + 'Permission denied.') + + d.delete() + + return Response(status.HTTP_200_OK) diff --git a/seahub/base/models.py b/seahub/base/models.py index 9f6dc8ce8e..dcbcdf9997 100644 --- a/seahub/base/models.py +++ b/seahub/base/models.py @@ -21,6 +21,23 @@ from fields import LowerCaseCharField logger = logging.getLogger(__name__) +class TimestampedModel(models.Model): + # A timestamp representing when this object was created. + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + + # A timestamp reprensenting when this object was last updated. + updated_at = models.DateTimeField(auto_now=True, db_index=True) + + class Meta: + abstract = True + + # By default, any model that inherits from `TimestampedModel` should + # be ordered in reverse-chronological order. We can override this on a + # per-model basis as needed, but reverse-chronological is a good + # default ordering for most models. + ordering = ['-created_at', '-updated_at'] + + class FileDiscuss(models.Model): """ Model used to represents the relationship between group message and file/dir. diff --git a/seahub/drafts/__init__.py b/seahub/drafts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/drafts/apps.py b/seahub/drafts/apps.py new file mode 100644 index 0000000000..1ff055c4d7 --- /dev/null +++ b/seahub/drafts/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class DraftsConfig(AppConfig): + name = 'drafts' diff --git a/seahub/drafts/migrations/0001_initial.py b/seahub/drafts/migrations/0001_initial.py new file mode 100644 index 0000000000..c659e3d712 --- /dev/null +++ b/seahub/drafts/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-09-03 08:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import seahub.base.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('tags', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Draft', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('username', seahub.base.fields.LowerCaseCharField(db_index=True, max_length=255)), + ('origin_repo_id', models.CharField(max_length=36)), + ('origin_file_version', models.CharField(max_length=100)), + ('draft_repo_id', models.CharField(max_length=36)), + ('draft_file_path', models.CharField(max_length=2048)), + ('origin_file_uuid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.FileUUIDMap')), + ], + ), + migrations.AlterUniqueTogether( + name='draft', + unique_together=set([('username', 'draft_repo_id')]), + ), + ] diff --git a/seahub/drafts/migrations/__init__.py b/seahub/drafts/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/drafts/models.py b/seahub/drafts/models.py new file mode 100644 index 0000000000..18651e6953 --- /dev/null +++ b/seahub/drafts/models.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import os +import posixpath + +from django.db import models +from seaserv import seafile_api + +from seahub.base.fields import LowerCaseCharField +from seahub.base.models import TimestampedModel +from seahub.base.templatetags.seahub_tags import email2nickname +from seahub.tags.models import FileUUIDMap +from seahub.utils import normalize_file_path +from seahub.utils.timeutils import datetime_to_isoformat_timestr +from .utils import create_user_draft_repo, get_draft_file_name + + +class DraftFileExist(Exception): + pass + + +class DraftFileConflict(Exception): + pass + + +class DraftManager(models.Manager): + def get_user_draft_repo_id(self, username): + r = self.filter(username=username).first() + if r is None: + return None + else: + return r.draft_repo_id + + def add(self, username, repo, file_path, file_id=None, org_id=-1): + file_path = normalize_file_path(file_path) + parent_path = os.path.dirname(file_path) + filename = os.path.basename(file_path) + file_uuid = FileUUIDMap.objects.get_or_create_fileuuidmap( + repo.id, parent_path, filename, is_dir=False) + + if file_id is None: + file_id = seafile_api.get_file_id_by_path(repo.id, file_path) + + # create draft repo if any + draft_repo_id = self.get_user_draft_repo_id(username) + if draft_repo_id is None: + draft_repo_id = create_user_draft_repo(username) + + # check draft file does not exists and copy origin file content to + # draft file + draft_file_name = get_draft_file_name(repo.id, file_path) + draft_file_path = '/' + draft_file_name + + if seafile_api.get_file_id_by_path(draft_repo_id, draft_file_path): + raise DraftFileExist + + seafile_api.copy_file(repo.id, file_uuid.parent_path, file_uuid.filename, + draft_repo_id, '/', draft_file_name, + username=username, need_progress=0, synchronous=1) + + draft = self.model(username=username, + origin_repo_id=repo.id, origin_file_uuid=file_uuid, + origin_file_version=file_id, + draft_repo_id=draft_repo_id, + draft_file_path=draft_file_path) + draft.save(using=self._db) + return draft + + +class Draft(TimestampedModel): + """Draft models enable user save file as drafts, and publish later. + """ + username = LowerCaseCharField(max_length=255, db_index=True) + origin_repo_id = models.CharField(max_length=36) + origin_file_uuid = models.ForeignKey(FileUUIDMap, on_delete=models.CASCADE) + origin_file_version = models.CharField(max_length=100) + draft_repo_id = models.CharField(max_length=36) + draft_file_path = models.CharField(max_length=2048) + + objects = DraftManager() + + # class Meta: + # unique_together = (('username', 'draft_repo_id'), ) + + def delete(self): + seafile_api.del_file(self.draft_repo_id, '/', + self.draft_file_path.lstrip('/'), self.username) + + super(Draft, self).delete() + + def publish(self): + # check whether origin file is updated + r_repo = seafile_api.get_repo(self.origin_repo_id) + if not r_repo: + raise DraftFileConflict + + origin_file_path = self.origin_file_uuid.parent_path + self.origin_file_uuid.filename + file_id = seafile_api.get_file_id_by_path(self.origin_repo_id, + origin_file_path) + if not file_id: + raise DraftFileConflict + + if file_id != self.origin_file_version: + raise DraftFileConflict + + # move draft file to origin file + seafile_api.move_file( + self.draft_repo_id, '/', self.draft_file_path.lstrip('/'), + self.origin_repo_id, self.origin_file_uuid.parent_path, + self.origin_file_uuid.filename, replace=1, + username=self.username, need_progress=0, synchronous=1 + ) + + self.delete() + + def to_dict(self): + uuid = self.origin_file_uuid + file_path = posixpath.join(uuid.parent_path, uuid.filename) # TODO: refactor uuid + + return { + 'id': self.pk, + 'owner': self.username, + 'owner_nickname': email2nickname(self.username), + 'origin_repo_id': self.origin_repo_id, + 'origin_file_path': file_path, + 'origin_file_version': self.origin_file_version, + 'draft_repo_id': self.draft_repo_id, + 'draft_file_path': self.draft_file_path, + 'created_at': datetime_to_isoformat_timestr(self.created_at), + 'updated_at': datetime_to_isoformat_timestr(self.updated_at), + } diff --git a/seahub/drafts/utils.py b/seahub/drafts/utils.py new file mode 100644 index 0000000000..cfe5973281 --- /dev/null +++ b/seahub/drafts/utils.py @@ -0,0 +1,23 @@ +import hashlib +import os + +from seaserv import seafile_api + +from seahub.utils import normalize_file_path + +def create_user_draft_repo(username, org_id=-1): + repo_name = 'Drafts' + if org_id > 0: + repo_id = seafile_api.create_org_repo(repo_name, '', username, + passwd=None, org_id=org_id) + else: + repo_id = seafile_api.create_repo(repo_name, '', username, + passwd=None) + return repo_id + +def get_draft_file_name(repo_id, file_path): + file_path = normalize_file_path(file_path) + file_name, file_ext = os.path.splitext(os.path.basename(file_path)) + md5 = hashlib.md5((repo_id + file_path).encode('utf-8')).hexdigest()[:10] + + return "%s-%s%s" % (file_name, md5, file_ext) diff --git a/seahub/settings.py b/seahub/settings.py index e861e79e3f..474ddca25b 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -226,6 +226,7 @@ INSTALLED_APPS = ( 'seahub.api2', 'seahub.avatar', 'seahub.contacts', + 'seahub.drafts', 'seahub.institutions', 'seahub.invitations', 'seahub.wiki', diff --git a/seahub/templates/drafts.html b/seahub/templates/drafts.html new file mode 100644 index 0000000000..f474d199a4 --- /dev/null +++ b/seahub/templates/drafts.html @@ -0,0 +1,7 @@ +{% extends "base_for_react.html" %} +{% load render_bundle from webpack_loader %} + +{% block extra_script %} +{% render_bundle 'drafts' %} +{% endblock %} + diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 82e5894d0d..2e2a1704f2 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -770,6 +770,7 @@
  • {% trans "Lock" %}
  • <% } %> <% } %> +
  • {% trans "New Draft" %}
  • +
  • + {% trans "Drafts" %} +