diff --git a/media/css/seahub.css b/media/css/seahub.css index ba892b545f..c60c0b7a0d 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -821,6 +821,20 @@ textarea:-moz-placeholder {/* for FF */ /* for separate pages */ /*myhome*/ +.personal-nav { + border-bottom:1px solid #ddd; + margin-bottom:16px; + margin-top:-12px; + text-align:right; +} +.personal-nav .item { + float:left; + font-size:15px; + margin:8px 16px 0 0; +} +.personal-nav a { + font-weight:normal; +} .home-profile .pic { margin-left:9px; } diff --git a/settings.py b/settings.py index a21cbcf810..6b005d5b27 100644 --- a/settings.py +++ b/settings.py @@ -131,6 +131,7 @@ INSTALLED_APPS = ( 'seahub.base', 'seahub.contacts', + 'seahub.wiki', 'seahub.group', 'seahub.notifications', # 'seahub.organizations', diff --git a/templates/myhome.html b/templates/myhome.html index d645b05d4d..99736cecbe 100644 --- a/templates/myhome.html +++ b/templates/myhome.html @@ -5,6 +5,19 @@ {% block sub_title %}{% trans "My Home" %} - {% endblock %} {% block nav_myhome_class %}class="cur"{% endblock %} + +{% block title_panel %} + +
+ +
+ +{% endblock %} + + {% block left_panel %}

{% trans "Account" %}

diff --git a/templates/snippets/my_owned_repos.html b/templates/snippets/my_owned_repos.html index fb0e38980f..da48a80cbd 100644 --- a/templates/snippets/my_owned_repos.html +++ b/templates/snippets/my_owned_repos.html @@ -1,6 +1,5 @@ {% load seahub_tags i18n %} {% load url from future %} -

{% trans "Libraries" %}

    diff --git a/templates/wiki/personal_wiki.html b/templates/wiki/personal_wiki.html new file mode 100644 index 0000000000..f6ed6c2a2d --- /dev/null +++ b/templates/wiki/personal_wiki.html @@ -0,0 +1,146 @@ +{% extends "myhome_base.html" %} + +{% load seahub_tags avatar_tags i18n %} +{% load url from future %} + +{% block sub_title %}{% trans "Personal Wiki" %}{% endblock %} +{% block nav_myhome_class %}class="cur"{% endblock %} + +{% block title_panel %} + +
    + +
    + +{% endblock %} + +{% block main_panel %} + + {% if not wiki_exists %} +
    +

    {% trans "You do not have personal wiki" %}

    +

    {% trans "Seafile Wiki enables you to take notes in a simple way. The contents of wiki is stored in a normal library with pre-defined file/folder structure. This enables you to edit your wiki in your desktop and then sync back to the server." %}

    + {% trans "Create Wiki Now" %} +
    +
    +

    {% trans "Create Wiki" %}

    +
    +
    +
    +
    +

    + +
    + + {% else %} +
    + + + + + + +
    + +
    +

    {{ page|capfirst }}

    +
    +

    {% blocktrans with modifier=latest_contributor|email2nickname modify_time=last_modified|translate_seahub_time %}Last modified by {{modifier}}, {{modify_time}}{% endblocktrans %}

    +
    + +
    +

    {% trans "New Page"%}

    +
    +
    +

    + +
    + {% endif %} +{% endblock main_panel %} + +{% block extra_script %} + + + +{% endblock %} diff --git a/templates/wiki/personal_wiki_pages.html b/templates/wiki/personal_wiki_pages.html new file mode 100644 index 0000000000..a5b4a248ff --- /dev/null +++ b/templates/wiki/personal_wiki_pages.html @@ -0,0 +1,53 @@ +{% extends "myhome_base.html" %} + +{% load seahub_tags avatar_tags group_avatar_tags i18n %} +{% load url from future %} + +{% block sub_title %}{% trans "Personal Wiki" %}{% endblock %} +{% block nav_myhome_class %}class="cur"{% endblock %} + +{% block title_panel %} + +
    + +
    + +{% endblock %} + +{% block main_panel %} + + + +
      + {% for page_slug, page in pages.items %} +
    • {{ page }}
    • + {% endfor %} +
    + +
    +

    {% trans "New Page"%}

    +
    +
    +

    + +
    + +{% endblock main_panel %} + +{% block extra_script %} + +{% endblock %} diff --git a/urls.py b/urls.py index f566387e37..4bb4f8c748 100644 --- a/urls.py +++ b/urls.py @@ -9,6 +9,9 @@ from seahub.views.repo import RepoView, RepoHistoryView from seahub.views.search import search from notifications.views import notification_list from group.views import group_list +from seahub.views.wiki import personal_wiki, personal_wiki_pages, \ + personal_wiki_create, personal_wiki_page_new, personal_wiki_page_edit, \ + personal_wiki_page_delete # Uncomment the next two lines to enable the admin: #from django.contrib import admin @@ -30,6 +33,14 @@ urlpatterns = patterns('', (r'^$', root), #url(r'^home/$', direct_to_template, { 'template': 'home.html' } ), url(r'^home/my/$', myhome, name='myhome'), + url(r'^home/wiki/$', personal_wiki, name='personal_wiki'), + url(r'^home/wiki/(?P[^/]+)/$', personal_wiki, name='personal_wiki'), + url(r'^home/wiki_pages/$', personal_wiki_pages, name='personal_wiki_pages'), + url(r'^home/wiki_create/$', personal_wiki_create, name='personal_wiki_create'), + url(r'^home/wiki_page_new/$', personal_wiki_page_new, name='personal_wiki_page_new'), + url(r'^home/wiki_page_edit/(?P[^/]+)$', personal_wiki_page_edit, name='personal_wiki_page_edit'), + url(r'^home/wiki_page_delete/(?P[^/]+)$', personal_wiki_page_delete, name='personal_wiki_page_delete'), + url(r'^home/public/reply/(?P[\d]+)/$', innerpub_msg_reply, name='innerpub_msg_reply'), url(r'^home/owner/(?P[^/]+)/$', ownerhome, name='ownerhome'), diff --git a/views/wiki.py b/views/wiki.py new file mode 100644 index 0000000000..970065d1a1 --- /dev/null +++ b/views/wiki.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +""" +File related views, including view_file, edit_file, view_history_file, +view_trash_file, view_snapshot_file +""" + +import os +import simplejson as json +import stat +import tempfile +import urllib +import urllib2 +import chardet + +from django.contrib.sites.models import Site, RequestSite +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseBadRequest, Http404, \ + HttpResponseRedirect +from django.shortcuts import render_to_response, redirect +from django.template import Context, loader, RequestContext +from django.template.loader import render_to_string +from django.utils.hashcompat import md5_constructor +from django.utils.http import urlquote +from django.utils.translation import ugettext as _ + +from auth.decorators import login_required + +import seaserv +from pysearpc import SearpcError + + +from seahub.wiki.models import PersonalWiki, WikiDoesNotExist, WikiPageMissing +from seahub.wiki import get_personal_wiki_page, get_personal_wiki_repo, \ + convert_wiki_link, get_wiki_pages +from seahub.wiki.forms import WikiCreateForm, WikiNewPageForm +from seahub.utils import get_file_contributors, render_error + +@login_required +def personal_wiki(request, page_name="home"): + username = request.user.username + wiki_exists = True + try: + content, repo, dirent = get_personal_wiki_page(username, page_name) + except WikiDoesNotExist: + wiki_exists = False + return render_to_response("wiki/personal_wiki.html", { + "wiki_exists": wiki_exists, + }, context_instance=RequestContext(request)) + except WikiPageMissing: + repo = get_personal_wiki_repo(username) + filename = clean_page_name(page_name) + '.md' + if not post_empty_file(repo.id, "/", filename, username): + return render_error(request, _("Failed to create wiki page. Please retry later.")) + return HttpResponseRedirect(reverse('personal_wiki', args=[page_name])) + else: + url_prefix = reverse('personal_wiki', args=[]) + content = convert_wiki_link(content, url_prefix, repo.id, username) + + # fetch file latest contributor and last modified + path = '/' + dirent.obj_name + file_path_hash = md5_constructor(urllib2.quote(path.encode('utf-8'))).hexdigest()[:12] + contributors, last_modified, last_commit_id = get_file_contributors(\ + repo.id, path.encode('utf-8'), file_path_hash, dirent.obj_id) + latest_contributor = contributors[0] if contributors else None + + return render_to_response("wiki/personal_wiki.html", { + "wiki_exists": wiki_exists, + "content": content, + "page": os.path.splitext(dirent.obj_name)[0], + "last_modified": last_modified, + "latest_contributor": latest_contributor, + "path": path, + "repo_id": repo.id, + }, context_instance=RequestContext(request)) + +@login_required +def personal_wiki_pages(request): + """ + List personal wiki pages. + """ + try: + repo = get_personal_wiki_repo(request.user.username) + pages = get_wiki_pages(repo) + except SearpcError: + return render_error(request, _('Internal Server Error')) + except WikiDoesNotExist: + return render_error(request, _('Wiki does not exists.')) + + return render_to_response("wiki/personal_wiki_pages.html", { + "pages": pages, + "repo_id": repo.id + }, context_instance=RequestContext(request)) + + +@login_required +def personal_wiki_create(request): + if request.method != 'POST': + raise Http404 + + content_type = 'application/json; charset=utf-8' + + def json_error(err_msg, status=400): + result = {'error': err_msg} + return HttpResponse(json.dumps(result), status=status, + content_type=content_type) + + form = WikiCreateForm(request.POST) + if not form.is_valid(): + return json_error(str(form.errors.values()[0])) + + # create group repo in user context + repo_name = form.cleaned_data['repo_name'] + repo_desc = form.cleaned_data['repo_desc'] + username = request.user.username + passwd = None + permission = "rw" + + repo_id = seaserv.create_repo(repo_name, repo_desc, username, passwd) + if not repo_id: + return json_error(_(u'Failed to create'), 500) + + PersonalWiki.objects.save_personal_wiki(username=username, repo_id=repo_id) + + # create home page + page_name = "home.md" + if not seaserv.post_empty_file(repo_id, "/", page_name, username): + return json_error(_(u'Failed to create home page. Please retry later'), 500) + + next = reverse('personal_wiki', args=[]) + return HttpResponse(json.dumps({'href': next}), content_type=content_type) + +@login_required +def personal_wiki_page_new(request, page_name="home"): + + if request.method == 'POST': + page_name = request.POST.get('page_name', '') + if not page_name: + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + page_name = clean_page_name(page_name) + + repo = find_wiki_repo(request, group) + if not repo: + return render_error(request, _('Wiki is not found.')) + + filename = page_name + ".md" + filepath = "/" + page_name + ".md" + + # check whether file exists + if get_file_id_by_path(repo.id, filepath): + return render_error(request, _('Page "%s" already exists.') % filename) + + if not post_empty_file(repo.id, "/", filename, request.user.username): + return render_error(request, _('Failed to create wiki page. Please retry later.')) + + url = "%s?p=%s&from=wiki_page_new&gid=%s" % ( + reverse('file_edit', args=[repo.id]), + urllib2.quote(filepath.encode('utf-8')), group.id) + return HttpResponseRedirect(url) + + +@login_required +def personal_wiki_page_edit(request, page_name="home"): + try: + repo = get_personal_wiki_repo(request.user.username) + except WikiDoesNotExist: + return render_error(request, _('Wiki is not found.')) + + filepath = "/" + page_name + ".md" + url = "%s?p=%s&from=personal_wiki_page_edit" % ( + reverse('file_edit', args=[repo.id]), + urllib2.quote(filepath.encode('utf-8'))) + + return HttpResponseRedirect(url) + + +@login_required +def personal_wiki_page_delete(request, page_name): + try: + repo = get_personal_wiki_repo(request.user.username) + except WikiDoesNotExist: + return render_error(request, _('Wiki is not found.')) + + file_name = page_name + '.md' + username = request.user.username + if del_file(repo.id, '/', file_name, username): + messages.success(request, 'Successfully deleted "%s".' % page_name) + else: + messages.error(request, 'Failed to delete "%s". Please retry later.' % page_name) + + return HttpResponseRedirect(reverse('personal_wiki', args=[])) diff --git a/wiki/__init__.py b/wiki/__init__.py new file mode 100644 index 0000000000..10ded49623 --- /dev/null +++ b/wiki/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from utils import get_personal_wiki_page, get_personal_wiki_repo, \ + convert_wiki_link, get_wiki_pages diff --git a/wiki/forms.py b/wiki/forms.py new file mode 100644 index 0000000000..8834b89245 --- /dev/null +++ b/wiki/forms.py @@ -0,0 +1,37 @@ +# encoding: utf-8 + +from django import forms +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from seaserv import is_valid_filename + +from utils import clean_page_name + +class WikiCreateForm(forms.Form): + """ + A form used to create wiki. + """ + repo_name = forms.CharField(max_length=settings.MAX_FILE_NAME, + error_messages={ + 'required': _(u'Name can\'t be empty'), + 'max_length': _(u'Name is too long (maximum is 255 characters)') + }) + repo_desc = forms.CharField(max_length=100, error_messages={ + 'required': _(u'Description can\'t be empty'), + 'max_length': _(u'Description is too long (maximum is 100 characters)') + }) + + def clean_repo_name(self): + repo_name = self.cleaned_data['repo_name'] + if not is_valid_filename(repo_name): + error_msg = _(u'"%s" is not a valid name') % repo_name + raise forms.ValidationError(error_msg) + else: + return repo_name + + +class WikiNewPageForm(forms.Form): + page_name = forms.CharField(max_length=500) + + def clean_page_name(self): + page_name = self.cleaned_data['page_name'] diff --git a/wiki/models.py b/wiki/models.py new file mode 100644 index 0000000000..67c7a803ff --- /dev/null +++ b/wiki/models.py @@ -0,0 +1,25 @@ +from django.db import models + +class WikiDoesNotExist(Exception): + pass + +class WikiPageMissing(Exception): + pass + +class PersonalWikiManager(models.Manager): + def save_personal_wiki(self, username, repo_id): + """ + Create or update group wiki. + """ + try: + wiki = self.get(username=username) + wiki.repo_id = repo_id + except self.model.DoesNotExist: + wiki = self.model(username=username, repo_id=repo_id) + wiki.save(using=self._db) + return wiki + +class PersonalWiki(models.Model): + username = models.CharField(max_length=256, unique=True) + repo_id = models.CharField(max_length=36) + objects = PersonalWikiManager() diff --git a/wiki/utils.py b/wiki/utils.py new file mode 100644 index 0000000000..f4fd03a889 --- /dev/null +++ b/wiki/utils.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +import os +import stat +import urllib2 + +import seaserv +from pysearpc import SearpcError +from seahub.utils import EMPTY_SHA1 +from seahub.utils.repo import list_dir_by_path +from seahub.utils.slugify import slugify +from seahub.utils import render_error, render_permission_error, string2list, \ + gen_file_get_url, get_file_type_and_ext, \ + get_file_contributors + +from models import WikiPageMissing, WikiDoesNotExist, \ + PersonalWiki + +__all__ = ["get_wiki_dirent"] + + + +SLUG_OK = "!@#$%^&()_+-,.;'" +def normalize_page_name(page_name): + # Remove special characters. Lower page name and replace spaces with '-'. + return slugify(page_name, ok=SLUG_OK) + +def clean_page_name(page_name): + # Remove special characters. Do not lower page name and spaces are allowed. + return slugify(page_name, ok=SLUG_OK, lower=False, spaces=True) + +def get_wiki_dirent(repo_id, page_name): + file_name = page_name + ".md" + repo = seaserv.get_repo(repo_id) + if not repo: + raise WikiDoesNotExist + cmmt = seaserv.get_commits(repo.id, 0, 1)[0] + if cmmt is None: + raise WikiPageMissing + dirs = list_dir_by_path(cmmt, "/") + if not dirs: + raise WikiPageMissing + else: + for e in dirs: + if stat.S_ISDIR(e.mode): + continue # skip directories + if normalize_page_name(file_name) == normalize_page_name(e.obj_name): + return e + raise WikiPageMissing + +def get_file_url(repo, obj_id, file_name): + repo_id = repo.id + access_token = seaserv.seafserv_rpc.web_get_access_token(repo_id, obj_id, + 'view', '') + url = gen_file_get_url(access_token, file_name) + return url + +def get_wiki_page(request, page_name): + repo = find_wiki_repo(request, group) + dirent = get_wiki_dirent(repo.id, page_name) + if not dirent: + raise WikiPageMissing + url = get_file_url(repo, dirent.obj_id, dirent.obj_name) + file_response = urllib2.urlopen(url) + content = file_response.read() + return content, repo.id, dirent + +def get_personal_wiki_repo(username): + try: + wiki = PersonalWiki.objects.get(username=username) + except PersonalWiki.DoesNotExist: + raise WikiDoesNotExist + repo = seaserv.get_repo(wiki.repo_id) + if not repo: + raise WikiDoesNotExist + return repo + +def get_personal_wiki_page(username, page_name): + repo = get_personal_wiki_repo(username) + dirent = get_wiki_dirent(repo.id, page_name) + url = get_file_url(repo, dirent.obj_id, dirent.obj_name) + file_response = urllib2.urlopen(url) + content = file_response.read() + return content, repo, dirent + +def get_wiki_pages(repo): + """ + return pages in hashtable {normalized_name: page_name} + """ + dir_id = seaserv.seafserv_threaded_rpc.get_dir_id_by_path(repo.id, '/') + dirs = seaserv.seafserv_threaded_rpc.list_dir(dir_id) + pages = {} + for e in dirs: + if stat.S_ISDIR(e.mode): + continue # skip directories + name, ext = os.path.splitext(e.obj_name) + if ext == '.md': + key = normalize_page_name(name) + pages[key] = name + return pages + + +def convert_wiki_link(content, url_prefix, repo_id, username): + import re + + def repl(matchobj): + if matchobj.group(2): # return origin string in backquotes + return matchobj.group(2) + + page_name = matchobj.group(1).strip() + filetype, fileext = get_file_type_and_ext(page_name) + if fileext == '': + # convert page_name that extension is missing to a markdown page + dirent = get_wiki_dirent(repo_id, page_name) + if dirent is not None: + a_tag = "%s" + return a_tag % (url_prefix + '/' + normalize_page_name(page_name), page_name) + else: + a_tag = '''%s''' + return a_tag % (url_prefix + '/' + page_name.replace('/', '-'), page_name) + elif filetype == IMAGE: + # load image to wiki page + path = "/" + page_name + filename = os.path.basename(path) + obj_id = get_file_id_by_path(repo_id, path) + if not obj_id: + # Replace '/' in page_name to '-', since wiki name can not + # contain '/'. + return '''%s''' % \ + (url_prefix + '/' + page_name.replace('/', '-'), page_name) + + token = seaserv.web_get_access_token(repo_id, obj_id, 'view', username) + return '%s' % (gen_file_get_url(token, filename), filename) + else: + from base.templatetags.seahub_tags import file_icon_filter + + # convert other types of filelinks to clickable links + path = "/" + page_name + icon = file_icon_filter(page_name) + s = reverse('repo_view_file', args=[repo_id]) + \ + '?p=' + urllib2.quote(smart_str(path)) + a_tag = '''%s %s''' + return a_tag % (MEDIA_URL, icon, icon, s, page_name) + + return re.sub(r'\[\[(.+)\]\]|(`.+`)', repl, content) + +