diff --git a/seahub/api2/endpoints/admin/revision_tag.py b/seahub/api2/endpoints/admin/revision_tag.py new file mode 100644 index 0000000000..8cbb0596f4 --- /dev/null +++ b/seahub/api2/endpoints/admin/revision_tag.py @@ -0,0 +1,83 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import re + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from django.utils.translation import ugettext as _ + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.utils import api_error +from seahub.base.accounts import User +from seahub.revision_tag.models import RevisionTags + +from seaserv import seafile_api + + +def check_tagname(tag_name): + return True if re.match('^[\.\w-]+$', tag_name, re.U) else False + + +class AdminTaggedItemsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request): + normal = True + user = request.GET.get('user', None) + if user is not None: + if not normal: + error_msg = "unsupported operation" + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + normal = False + try: + User.objects.get(email=user) + except User.DoesNotExist: + error_msg = "User %s not found" % user + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + revision_tags = RevisionTags.objects.get_all_tags_by_creator(user) + + repo_id = request.GET.get('repo_id', None) + if repo_id is not None: + if not normal: + error_msg = "unsupported operation" + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + normal = False + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = "Library %s not found" % repo_id + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + revision_tags = RevisionTags.objects.get_all_tags_by_repo(repo_id) + + tag_name = request.GET.get('tag_name', None) + if tag_name is not None: + if not normal: + error_msg = "unsupported operation" + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + normal = False + if not check_tagname(tag_name): + error_msg = "Tag %s invalid" % tag_name + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + revision_tags = RevisionTags.objects.get_all_tags_by_tagname(tag_name) + + tag_contains = request.GET.get('tag_contains', None) + if tag_contains is not None: + if not normal: + error_msg = "unsupported operation" + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + normal = False + if not check_tagname(tag_contains): + error_msg = "key word %s invalid" % tag_contains + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + revision_tags = RevisionTags.objects.get_all_tags_by_key(tag_contains) + + if normal: + revision_tags = RevisionTags.objects.all() + + revision_tags = sorted(revision_tags, key=lambda revision_tags: revision_tags['tag']) + return Response([revision_tag.to_dict() for revision_tag in revision_tags]) diff --git a/seahub/api2/endpoints/revision_tag.py b/seahub/api2/endpoints/revision_tag.py new file mode 100644 index 0000000000..1f1260b872 --- /dev/null +++ b/seahub/api2/endpoints/revision_tag.py @@ -0,0 +1,110 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import re + +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 django.utils.translation import ugettext as _ + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.utils import api_error +from seahub.revision_tag.models import Tags, RevisionTags +from seahub.views import check_folder_permission + +from seaserv import seafile_api + + +def check_parameter(func): + def _decorated(view, request, *args, **kwargs): + if request.method == "POST": + repo_id = request.data.get('repo_id', '') + tag_names = request.data.get('tag_names', '') + if not tag_names: + error_msg = _("Tag can not be empty") + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + names = [name.strip() for name in tag_names.split(',')] + tag_names = [] + for name in names: + if not check_tagname(name): + error_msg = _("Tag can only contains letters, numbers, dot, hyphen or underscore") + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + tag_names.append(name) + elif request.method == "DELETE": + repo_id = request.GET.get('repo_id', '') + tag_name = request.GET.get('tag_name', '') + if not tag_name: + error_msg = _("Tag can not be empty") + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + if tag_name not in Tags.objects.get_all_tag_name(): + error_msg = "Tag %s not found" % tag_name + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + if not check_tagname(tag_name): + error_msg = _('Tag can only contains letters, numbers, dot, hyphen or underscore') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if not repo_id: + error_msg = "Repo can not be empty" + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = "Library %s not found" % repo_id + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if check_folder_permission(request, repo_id, '/') != 'rw': + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + return func(view, request, *args, **kwargs) + return _decorated + + +def check_tagname(tagname): + return True if re.match('^[\.\w-]+$', tagname, re.U) else False + + +class TaggedItemsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + @check_parameter + def post(self, request): + repo_id = request.POST.get('repo_id') + tag_names = request.POST.get('tag_names').split(',') + repo = seafile_api.get_repo(repo_id) + if repo.head_commit_id is not None: + commit_id = repo.head_commit_id + else: + commit_id = seafile_api.get_commit_list(repo_id, 0, 1)[0].id + for name in tag_names: + revision_tag, created = RevisionTags.objects.create_revision_tag( + repo_id, commit_id, name.strip(), request.user.username) + return Response({"success": True}, status=status.HTTP_200_OK) + + @check_parameter + def delete(self, request, repo_id, tag_name): + repo_id = request.GET.get('repo_id') + tag_name = request.GET.get('tag_name') + commit_id = None + if RevisionTags.objects.delete_revision_tag(repo_id, commit_id, + tag_name): + return Response({"success": True}, status=status.HTTP_200_OK) + else: + return Response({"success": True}, status=status.HTTP_202_ACCEPTED) + + +class TagNamesView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request): + revision_tags = [tag["tag"].name for tag in RevisionTags.objects.\ + get_all_tags_by_creator(request.user.username)] + revision_tags = sorted(revision_tags) + + return Response(revision_tags) diff --git a/seahub/revision_tag/__init__.py b/seahub/revision_tag/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/revision_tag/models.py b/seahub/revision_tag/models.py new file mode 100644 index 0000000000..23549e3f32 --- /dev/null +++ b/seahub/revision_tag/models.py @@ -0,0 +1,100 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import os +from django.db import models +from django.core.urlresolvers import reverse + +from seahub.base.fields import LowerCaseCharField +from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email +from seahub.utils.timeutils import timestamp_to_isoformat_timestr + +import seaserv +from seaserv import seafile_api + + +########## manager +class TagsManager(models.Manager): + def get_all_tag_name(self): + return [tag.name for tag in super(TagsManager, self).all()] + + def get_or_create_tag(self, tagname): + try: + tag = super(TagsManager, self).get(name=tagname) + return tag + except self.model.DoesNotExist: + tag = self.model(name=tagname) + tag.save() + return tag + +class RevisionTagsManager(models.Manager): + def get_one_revision_tag(self, repo_id, commit_id, tag_name): + try: + return super(RevisionTagsManager, self).get( + repo_id=repo_id, + revision_id=commit_id, + tag__name=tag_name) + except: + return None + + def get_all_tags_by_repo(self, repo_id): + return super(RevisionTagsManager, self).filter(repo_id=repo_id) + + def get_all_tags_by_creator(self, creator): + return super(RevisionTagsManager, self).filter(username=creator) + + def get_all_tags_by_key(self, key): + return super(RevisionTagsManager, self).filter(tag__name__contains=key) + + def get_all_tags_by_tagname(self, tag_name): + return super(RevisionTagsManager, self).filter(tag__name=tag_name) + + def create_revision_tag(self, repo_id, commit_id, tag_name, creator): + revision_tag = self.get_one_revision_tag(repo_id, commit_id, tag_name) + exists = False + if revision_tag: + return revision_tag, False + else: + tag = Tags.objects.get_or_create_tag(tag_name) + revision_tag = self.model(repo_id=repo_id, revision_id=commit_id, tag=tag, username=creator) + revision_tag.save() + return revision_tag, True + + def delete_revision_tag(self, repo_id, commit_id, tag_name): + revision_tag = self.get_one_revision_tag(repo_id, commit_id, tag_name) + if not revision_tag: + return False + else: + revision_tag.delete() + return True + +########## models +class Tags(models.Model): + name = models.CharField(max_length=255, unique=True) + objects = TagsManager() + +class RevisionTags(models.Model): + repo_id = models.CharField(max_length=36, db_index=True) + path = models.TextField(default='/') + revision_id = models.CharField(max_length=255, db_index=True) + tag = models.ForeignKey("Tags", on_delete=models.CASCADE) + username = LowerCaseCharField(max_length=255, db_index=True) + objects = RevisionTagsManager() + + def to_dict(self): + repo = seafile_api.get_repo(self.repo_id) + commit = seaserv.get_commit(repo.id, repo.revision, self.revision_id) + email = commit.creator_name + return {"tag":self.tag.name, + "tag_creator": self.username, + "revision": { + "repo_id": self.repo_id, + "commit_id": self.revision_id, + "email": email, + "name": email2nickname(email), + "contact_email": email2contact_email(email), + "time": timestamp_to_isoformat_timestr(commit.ctime), + "description": commit.desc, + "link": reverse("repo_history_view", args=[self.repo_id])+"?commit_id=%s"%self.revision_id + }} + + def __getitem__(self, item): + return getattr(self, item) diff --git a/seahub/settings.py b/seahub/settings.py index 6cb60f0df5..a76d7eeb21 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -225,6 +225,7 @@ INSTALLED_APPS = ( 'seahub.admin_log', 'seahub.wopi', 'seahub.tags', + 'seahub.revision_tag', ) # Enabled or disable constance(web settings). diff --git a/seahub/urls.py b/seahub/urls.py index 7f9a09bdb2..96ebf6702f 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -47,8 +47,10 @@ from seahub.api2.endpoints.notifications import NotificationsView, NotificationV from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView from seahub.api2.endpoints.user_avatar import UserAvatarView +from seahub.api2.endpoints.revision_tag import TaggedItemsView,TagNamesView # Admin +from seahub.api2.endpoints.admin.revision_tag import AdminTaggedItemsView from seahub.api2.endpoints.admin.login import Login from seahub.api2.endpoints.admin.file_audit import FileAudit from seahub.api2.endpoints.admin.file_update import FileUpdate @@ -211,6 +213,10 @@ urlpatterns = patterns( url(r'^api/v2.1/upload-links/$', UploadLinks.as_view(), name='api-v2.1-upload-links'), url(r'^api/v2.1/upload-links/(?P[a-f0-9]+)/$', UploadLink.as_view(), name='api-v2.1-upload-link'), + ## user::revision-tags + url(r'^api/v2.1/revision-tags/tagged-items/$', TaggedItemsView.as_view(), name='api-v2.1-revision-tags-tagged-items'), + url(r'^api/v2.1/revision-tags/tag-names/$', TagNamesView.as_view(), name='api-v2.1-revision-tags-tag-names'), + ## user::repos url(r'^api/v2.1/repos/batch/$', ReposBatchView.as_view(), name='api-v2.1-repos-batch'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'), @@ -246,6 +252,9 @@ urlpatterns = patterns( ## admin::sysinfo url(r'^api/v2.1/admin/sysinfo/$', SysInfo.as_view(), name='api-v2.1-sysinfo'), + ## admin::revision-tags + url(r'^api/v2.1/admin/revision-tags/tagged-items/$', AdminTaggedItemsView.as_view(), name='api-v2.1-admin-revision-tags-tagged-items'), + ## admin::devices url(r'^api/v2.1/admin/devices/$', AdminDevices.as_view(), name='api-v2.1-admin-devices'), url(r'^api/v2.1/admin/device-errors/$', AdminDeviceErrors.as_view(), name='api-v2.1-admin-device-errors'), diff --git a/tests/api/endpoints/admin/test_revision_tag.py b/tests/api/endpoints/admin/test_revision_tag.py new file mode 100644 index 0000000000..73e3555f2d --- /dev/null +++ b/tests/api/endpoints/admin/test_revision_tag.py @@ -0,0 +1,81 @@ +import os +import json + +from mock import patch +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase +from seaserv import seafile_api + + +class RevisionTagsTest(BaseTestCase): + def setUp(self): + self.login_as(self.admin) + self.url = reverse("api-v2.1-admin-revision-tags-tagged-items") + self.url_create = reverse("api-v2.1-revision-tags-tagged-items") + self.repo = seafile_api.get_repo(self.create_repo( + name="test_repo", + desc="", + username=self.admin.username, + passwd=None + )) + self.tag_name = "test_tag_name" + + def test_get_revision_by_user(self): + resp = self.client.post(self.url_create, { + "tag_names": self.tag_name, + "repo_id": self.repo.id, + }) + assert resp.status_code in [200, 201] + resp = self.client.get(self.url+"?user="+self.admin.username) + assert self.tag_name in [tag["tag"] for tag in resp.data] + resp = self.client.get(self.url+"?user="+self.user.username) + assert not self.tag_name in [tag["tag"] for tag in resp.data] + + def test_get_revision_by_repo_id(self): + p_repo = seafile_api.get_repo(self.create_repo( + name="test_repo", + desc="", + username=self.admin.username, + passwd=None + )) + resp = self.client.post(self.url_create, { + "tag_names": self.tag_name, + "repo_id": self.repo.id, + }) + assert resp.status_code in [200, 201] + resp = self.client.get(self.url+"?repo_id="+self.repo.id) + assert self.tag_name in [tag["tag"] for tag in resp.data] + resp = self.client.get(self.url+"?repo_id="+p_repo.id) + assert not self.tag_name in [tag["tag"] for tag in resp.data] + + def test_revisin_by_tag_name(self): + resp = self.client.post(self.url_create, { + "tag_names": self.tag_name, + "repo_id": self.repo.id, + }) + assert resp.status_code in [200, 201] + resp = self.client.get(self.url+"?tag_name="+self.tag_name) + assert self.tag_name in [tag["tag"] for tag in resp.data] + resp = self.client.get(self.url+"?tag_name=Hello") + assert not self.tag_name in [tag["tag"] for tag in resp.data] + + def test_revisin_by_tag_contains(self): + resp = self.client.post(self.url_create, { + "tag_names": self.tag_name, + "repo_id": self.repo.id, + }) + assert resp.status_code in [200, 201] + resp = self.client.get(self.url+"?tag_contains="+self.tag_name[:-2]) + assert self.tag_name in [tag["tag"] for tag in resp.data] + resp = self.client.get(self.url+"?tag_contains=Hello") + assert not self.tag_name in [tag["tag"] for tag in resp.data] + + def test_revision_all(self): + resp = self.client.post(self.url_create, { + "tag_names": self.tag_name, + "repo_id": self.repo.id, + }) + assert resp.status_code in [200, 201] + resp = self.client.get(self.url) + assert self.tag_name in [tag["tag"] for tag in resp.data] diff --git a/tests/api/endpoints/test_revision_tag.py b/tests/api/endpoints/test_revision_tag.py new file mode 100644 index 0000000000..77ea35fe20 --- /dev/null +++ b/tests/api/endpoints/test_revision_tag.py @@ -0,0 +1,42 @@ +import os +import json + +from mock import patch +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase +from seaserv import seafile_api + + +class RevisionTagsTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.tag_name = "test_tag_name" + self.repo = seafile_api.get_repo(self.create_repo( + name="test_repo", + desc="", + username=self.user.username, + passwd=None + )) + self.url = reverse("api-v2.1-revision-tags-tagged-items") + self.url_get = reverse("api-v2.1-revision-tags-tag-names") + + @patch('seahub.api2.endpoints.revision_tag.check_folder_permission') + def test_revision_tags(self, mock_permission): + mock_permission.return_value = 'rw' + c_resp = self.client.post(self.url, { + "tag_names": self.tag_name, + "repo_id": self.repo.repo_id, + }) + assert c_resp.status_code in [200, 201] + g_resp = self.client.get(self.url_get+"?name_only=true") + assert g_resp.status_code == 200 + assert self.tag_name in g_resp.data + + #d_resp = self.client.delete(self.url+"?repo_id="+str(self.repo.repo_id)+ + # "&tag_name="+str(self.tag_name)) + #assert d_resp.status_code in [200, 202] + + #g_resp = self.client.get(self.url_get+"?name_only=true") + #assert g_resp.status_code == 200 + #assert self.tag_name not in g_resp.data