김재형

Merge remote-tracking branch 'origin/develop' into feature/frontend

...@@ -26,3 +26,4 @@ __pycache__ ...@@ -26,3 +26,4 @@ __pycache__
26 npm-debug.log* 26 npm-debug.log*
27 yarn-debug.log* 27 yarn-debug.log*
28 yarn-error.log* 28 yarn-error.log*
29 +.idea
......
1 +from django.contrib import admin
2 +
3 +from .models import Item, SharedItem, User
4 +
5 +admin.site.register(Item)
6 +admin.site.register(SharedItem)
7 +admin.site.register(User)
...\ No newline at end of file ...\ No newline at end of file
1 +# Generated by Django 3.0.6 on 2020-06-11 14:54
2 +
3 +from django.db import migrations, models
4 +
5 +
6 +class Migration(migrations.Migration):
7 +
8 + initial = True
9 +
10 + dependencies = [
11 + ]
12 +
13 + operations = [
14 + migrations.CreateModel(
15 + name='Item',
16 + fields=[
17 + ('item_id', models.AutoField(primary_key=True, serialize=False)),
18 + ('is_folder', models.BooleanField(default=False)),
19 + ('name', models.CharField(max_length=50)),
20 + ('file_type', models.CharField(max_length=100, null=True)),
21 + ('path', models.TextField()),
22 + ('parent', models.IntegerField()),
23 + ('user_id', models.IntegerField()),
24 + ('size', models.IntegerField()),
25 + ('is_deleted', models.BooleanField(default=False)),
26 + ('created_time', models.DateTimeField(auto_now=True)),
27 + ('updated_time', models.DateTimeField(null=True)),
28 + ('status', models.BooleanField()),
29 + ],
30 + options={
31 + 'ordering': ['item_id'],
32 + },
33 + ),
34 + migrations.CreateModel(
35 + name='SharedItem',
36 + fields=[
37 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
38 + ('item_id', models.IntegerField()),
39 + ('expires', models.DateTimeField()),
40 + ('password', models.CharField(max_length=20)),
41 + ('created_time', models.DateTimeField(auto_now=True)),
42 + ],
43 + options={
44 + 'ordering': ['item_id'],
45 + },
46 + ),
47 + migrations.CreateModel(
48 + name='User',
49 + fields=[
50 + ('int_id', models.AutoField(primary_key=True, serialize=False)),
51 + ('user_id', models.CharField(max_length=50)),
52 + ('name', models.CharField(max_length=50)),
53 + ('password', models.CharField(max_length=20)),
54 + ('total_size', models.IntegerField()),
55 + ('current_size', models.IntegerField()),
56 + ('created_time', models.DateTimeField(auto_now=True)),
57 + ],
58 + options={
59 + 'ordering': ['int_id'],
60 + },
61 + ),
62 + ]
1 +# Generated by Django 3.0.6 on 2020-06-11 15:29
2 +
3 +from django.db import migrations, models
4 +
5 +
6 +class Migration(migrations.Migration):
7 +
8 + dependencies = [
9 + ('api', '0001_initial'),
10 + ]
11 +
12 + operations = [
13 + migrations.AddField(
14 + model_name='user',
15 + name='root_folder',
16 + field=models.IntegerField(null=True),
17 + ),
18 + migrations.AlterField(
19 + model_name='item',
20 + name='parent',
21 + field=models.IntegerField(null=True),
22 + ),
23 + ]
1 +from django.db import models
2 +
3 +# Create your models here.
4 +class Item(models.Model):
5 + item_id = models.AutoField(primary_key = True)
6 + is_folder = models.BooleanField(default = False)
7 + name = models.CharField(max_length = 50)
8 + file_type = models.CharField(max_length=100, null=True) # signed_url 생성을 위해 file type 세팅
9 + path = models.TextField()
10 + #parent = models.ForeignKey('Item', on_delete=models.CASCADE, null=True) #related_name
11 + parent = models.IntegerField(null=True) # root 폴더의 경우 null임
12 + user_id = models.IntegerField()
13 + size = models.IntegerField()
14 + is_deleted = models.BooleanField(default = False)
15 + created_time = models.DateTimeField(auto_now=True)
16 + updated_time = models.DateTimeField(null=True)
17 + status = models.BooleanField()
18 +
19 + #file = models.FileField(upload_to = \path)
20 +
21 + class Meta:
22 + ordering = ['item_id']
23 +
24 +
25 +class SharedItem(models.Model):
26 + item_id = models.IntegerField()
27 + #file_id?
28 + expires = models.DateTimeField()
29 + password = models.CharField(max_length = 20)
30 + created_time = models.DateTimeField(auto_now=True)
31 + class Meta:
32 + ordering = ['item_id']
33 +
34 +
35 +class User(models.Model):
36 + int_id = models.AutoField(primary_key = True)
37 + user_id = models.CharField(max_length = 50)
38 + name = models.CharField(max_length = 50)
39 + password = models.CharField(max_length = 20)
40 + root_folder = models.IntegerField(null=True)
41 + total_size = models.IntegerField()
42 + current_size = models.IntegerField()
43 + created_time = models.DateTimeField(auto_now=True)
44 + class Meta:
45 + ordering = ['int_id']
1 +from django.contrib.auth.models import Group
2 +from rest_framework import serializers
3 +from .models import Item, SharedItem,User
4 +
5 +
6 +class UserSerializer(serializers.HyperlinkedModelSerializer):
7 + class Meta:
8 + model = User
9 + fields = '__all__'
10 +
11 +class GroupSerializer(serializers.HyperlinkedModelSerializer):
12 + class Meta:
13 + model = Group
14 + fields = ['url', 'name']
15 +
16 +class ItemSerializer(serializers.ModelSerializer):
17 + class Meta:
18 + model = Item
19 + fields = '__all__'
20 +
1 +import mimetypes
2 +import json
3 +import os
4 +from datetime import datetime, timedelta
5 +
6 +import boto3
7 +
8 +from django.core import serializers
9 +from django.views.decorators.csrf import csrf_exempt
10 +from rest_framework import viewsets
11 +from rest_framework import permissions
12 +from rest_framework.response import Response
13 +from rest_framework.decorators import action
14 +from rest_framework.permissions import IsAuthenticated, AllowAny
15 +
16 +from .models import Item, SharedItem, User
17 +from .serializers import UserSerializer,GroupSerializer,ItemSerializer
18 +from rest_framework import status
19 +from annoying.functions import get_object_or_None
20 +from django.conf import settings
21 +import jwt
22 +from django.http import HttpResponse, JsonResponse
23 +
24 +
25 +class UserViewSet(viewsets.ModelViewSet):
26 + """
27 + API endpoint that allows users to be viewed or edited.
28 + """
29 + queryset = User.objects.all().order_by('-date_joined')
30 + serializer_class = UserSerializer
31 + permission_classes = [permissions.IsAuthenticatedOrReadOnly, permissions.AllowAny,
32 + # IsOwnerOrReadOnly
33 + ]
34 + permission_classes_by_action = {'get': [permissions.AllowAny],
35 + 'destroy': [permissions.AllowAny]}
36 + @csrf_exempt
37 + @action(detail=False, methods=['POST'], permission_classes=[permissions.AllowAny], url_path='signup', url_name='singup')
38 + def signup(self, request):
39 + user_id = request.POST.get('user_id', '')
40 + name = request.POST.get('name', '')
41 + password = request.POST.get('password', '')
42 + user = get_object_or_None(User, user_id=user_id)
43 + if user == None:
44 + user = User(user_id = user_id, name = name, password = password, total_size=100000, current_size = 0)
45 + user.save()
46 + root = Item(is_folder=True, name="root", file_type="folder", path="", user_id=user.int_id, size=0,
47 + status=True)
48 + root.save()
49 + return Response({
50 + 'message': 'user created',
51 + 'int_id': user.int_id,
52 + 'user_id': user.user_id,
53 + 'name': user.name,
54 + 'root_folder':root.item_id,
55 + 'total_size': user.total_size,
56 + 'current_size': user.current_size,
57 + 'created_time': user.created_time
58 + },
59 + status=status.HTTP_200_OK,
60 + )
61 + else:
62 + return Response({'message': 'user is already exist.'}, status=status.HTTP_204_NO_CONTENT)
63 +
64 + @csrf_exempt
65 + @action(methods=['post'], detail=False, permission_classes=[permissions.AllowAny],
66 + url_path='login', url_name='login')
67 + def login(self, request):
68 + if not request.data:
69 + return Response({'Error': "Please provide user_id/password"}, status=status.HTTP_400_BAD_REQUEST)
70 + user_id = request.POST['user_id']
71 + password = request.POST['password']
72 + try:
73 + user = User.objects.get(user_id=user_id, password=password)
74 + except User.DoesNotExist:
75 + return Response({'Error': "Invalid user_id/password"}, status=status.HTTP_400_BAD_REQUEST)
76 + if user:
77 + payload1 = {
78 + 'int_id': user.int_id,
79 + 'user_id': user.user_id,
80 + 'exp': datetime.utcnow() + timedelta(seconds=300)
81 + }
82 + payload2 = {
83 + 'int_id': user.int_id,
84 + 'user_id': user.user_id,
85 + 'exp': datetime.utcnow() + timedelta(days=5)
86 + }
87 + access = jwt.encode(payload1, settings.SECRET_KEY, algorithm='HS256').decode('utf-8')
88 + refresh = jwt.encode(payload2, settings.SECRET_KEY, algorithm='HS256').decode('utf-8')
89 + exp = jwt.decode(access, settings.SECRET_KEY, algorithm='HS256')['exp']
90 + token = {'access': access,
91 + 'refresh': refresh,
92 + 'exp': exp}
93 + return JsonResponse(
94 + token,
95 + status=status.HTTP_200_OK,
96 + )
97 + else:
98 + return JsonResponse(
99 + {'Error': "Invalid credentials"},
100 + status=status.HTTP_400_BAD_REQUEST,
101 + )
102 + return JsonResponse(status=status.HTTP_405_METHOD_NOT_ALLOWED)
103 +
104 + def get(self, request, pk):
105 + user = User.objects.filter(int_id=pk)
106 + data = serializers.serialize("json", user)
107 + json_data = json.loads(data)
108 + res = json_data[0]['fields']
109 + res['id']=json_data[0]['pk']
110 + return Response({'data': res}, status=status.HTTP_200_OK)
111 +
112 + def get_permissions(self):
113 + try:
114 + # return permission_classes depending on `action`
115 + return [permission() for permission in self.permission_classes_by_action[self.action]]
116 + except KeyError:
117 + # action is not set return default permission_classes
118 + return [permission() for permission in self.permission_classes]
119 +
120 +
121 +class ItemViewSet(viewsets.ViewSet):
122 +
123 + queryset = Item.objects.all()
124 + serializer_class = ItemSerializer
125 + permission_classes = [permissions.IsAuthenticatedOrReadOnly, permissions.AllowAny,
126 + #IsOwnerOrReadOnly
127 + ]
128 + permission_classes_by_action = {'get': [permissions.AllowAny],
129 + 'destroy': [permissions.AllowAny]}
130 +
131 + # url: items/search
132 + @action(methods=['GET'], detail=False, permission_classes=[AllowAny], url_path='search', url_name='search')
133 + def search(self, request):
134 + if request.method == 'GET':
135 + keyword = request.GET.get('keyword', '')
136 + user_id = request.GET.get('user_id', '')
137 + item_list = Item.objects.filter(name__icontains = keyword, user_id = user_id )
138 +
139 + data = serializers.serialize("json", item_list)
140 + json_data = json.loads(data)
141 + res = []
142 + for i in json_data:
143 + t = i['fields']
144 + t['id'] = i['pk']
145 + res.append(t)
146 + return Response({'data': {'list' : res}}, status=status.HTTP_200_OK)
147 +
148 + # url: items/11/
149 + # 마지막 slash도 써주어야함
150 + def get(self, request, pk):
151 + item = Item.objects.filter(item_id=pk)
152 + data = serializers.serialize("json", item)
153 + json_data = json.loads(data)
154 + res = json_data[0]['fields']
155 + res['id']=json_data[0]['pk']
156 + return Response({'data': res}, status=status.HTTP_200_OK)
157 +
158 + # url: items/11/
159 + # 마지막 slash도 써주어야함
160 + def destroy(self, request, pk):
161 + if request.method == 'DELETE':
162 + print(pk)
163 + item = get_object_or_None(Item, item_id=pk)
164 + if item != None:
165 + if item.is_folder == True: # 폴더는 삭제 안되도록 처리
166 + return Response({'message': 'This item is folder.'}, status=status.HTTP_200_OK)
167 + item.is_deleted = True
168 + item.save()
169 + # item.delete() 이거 하면 완전 삭제되어버림 is deleted True 면 휴지통에서 리스트 조회할 수 있도록!
170 + return Response({'message': 'delete complete'},status=status.HTTP_200_OK)
171 + return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
172 +
173 + # url: items/11/move
174 + # 마지막 slash도 써주어야함
175 + @action(methods=['POST'], detail=True, permission_classes=[AllowAny], url_path='move', url_name='move')
176 + def move(self, request, pk):
177 + if request.method == 'POST':
178 + parent_id = request.POST.get('parent', '')
179 + name = request.POST.get('name','')
180 + parent = get_object_or_None(Item, item_id=parent_id)
181 + if parent != None and parent.is_folder == True:
182 + child = get_object_or_None(Item, item_id=pk)
183 + if child == None:
184 + return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
185 + child.parent = parent_id
186 + child.save()
187 + child = Item.objects.filter(item_id = pk)
188 + child_data = serializers.serialize("json", child)
189 + json_child = json.loads(child_data)
190 + res = json_child[0]['fields']
191 + res['id'] = pk
192 + parent = Item.objects.filter(item_id = parent_id)
193 + parent_data = serializers.serialize("json", parent)
194 + json_parent = json.loads(parent_data)[0]['fields']
195 + res['parentInfo'] = json_parent
196 + return Response({'data': res}, status=status.HTTP_200_OK)
197 + if parent == None:
198 + return Response({'message': 'parent is not existed.'}, status=status.HTTP_200_OK)
199 + if parent.is_folder == False:
200 + return Response({'message': 'parent is not folder.'}, status=status.HTTP_200_OK)
201 + return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
202 +
203 + @action(methods=['POST'], detail=True, permission_classes=[AllowAny], url_path='copy', url_name='copy')
204 + def copy(self, request, pk):
205 + if request.method == 'POST':
206 + parent_id = request.POST.get('parent', '')
207 + parent = get_object_or_None(Item, item_id=parent_id)
208 + if parent != None and parent.is_folder == True:
209 + child = get_object_or_None(Item, item_id=pk)
210 + if child == None:
211 + return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
212 + if child.is_folder == True:
213 + return Response({'message': 'item is folder'}, status=status.HTTP_204_NO_CONTENT)
214 + copiedName = child.name + "_복사본_" + str(datetime.now().strftime('%Y-%m-%d %H:%M'))
215 + copiedItem = Item(is_folder = False, name = copiedName, path =child.path, parent = parent_id, user_id= child.user_id, size=child.size, status=child.status)
216 + copiedItem.save()
217 +
218 + copiedItem = Item.objects.filter(name = copiedName)
219 + copied_data = serializers.serialize("json", copiedItem)
220 + json_data = json.loads(copied_data)
221 + res = json_data[0]['fields']
222 + res['id'] = json_data[0]['pk']
223 + parent = Item.objects.filter(item_id = parent_id)
224 + parent_data = serializers.serialize("json", parent)
225 + json_parent = json.loads(parent_data)[0]['fields']
226 + res['parentInfo'] = json_parent
227 + return Response({'data': res}, status=status.HTTP_200_OK)
228 + if parent == None:
229 + return Response({'message': 'parent is not existed.'}, status=status.HTTP_200_OK)
230 + if parent.is_folder == False:
231 + return Response({'message': 'parent is not folder.'}, status=status.HTTP_200_OK)
232 + return Response({'message': 'item is not existed.'}, status=status.HTTP_204_NO_CONTENT)
233 +
234 + def get_permissions(self):
235 + try:
236 + # return permission_classes depending on `action`
237 + return [permission() for permission in self.permission_classes_by_action[self.action]]
238 + except KeyError:
239 + # action is not set return default permission_classes
240 + return [permission() for permission in self.permission_classes]
241 +
242 + # url: items/{key}/children/
243 + @action(methods=['GET', 'POST'], detail=True, permission_classes=[AllowAny],
244 + url_path='children', url_name='children')
245 + def children(self, request, pk):
246 + if request.method == 'GET':
247 + children = Item.objects.filter(parent = pk, is_deleted=False)
248 + children_data = serializers.serialize("json", children)
249 + json_children = json.loads(children_data)
250 + parent = Item.objects.filter(item_id=pk) #item
251 + parent_data = serializers.serialize("json", parent)
252 + json_parent = json.loads(parent_data)[0]['fields']
253 + res = json_parent
254 + res['id'] = pk
255 + children_list = []
256 + for i in json_children:
257 + t = i['fields']
258 + t['id'] = i['pk']
259 + children_list.append(t)
260 + res['list'] = children_list
261 + return Response({'data': res}, status=status.HTTP_200_OK)
262 + if request.method == 'POST':
263 + name = request.POST.get('name', '')
264 + user_id = request.GET.get('user_id', '')
265 + item = Item(is_folder=True, name=name, file_type="folder", path="", parent=pk, user_id=user_id, size=0, status=True)
266 + item.save()
267 + item = Item.objects.filter(item_id = item.item_id)
268 + item_data = serializers.serialize("json", item)
269 + json_item = json.loads(item_data)
270 + res = json_item[0]['fields']
271 + res['id']=json_item[0]['pk']
272 + res['inside_folder_list'] = []
273 + res['inside_file_list'] = []
274 + return Response({'data': res}, status=status.HTTP_200_OK)
275 +
276 +
277 +class SharedItemViewSet(viewsets.ModelViewSet):
278 +
279 + queryset = SharedItem.objects.all()
280 + # serializer_class = SharedItemSerializer
281 + permission_classes = [permissions.IsAuthenticatedOrReadOnly, permissions.AllowAny,
282 + # IsOwnerOrReadOnly
283 + ]
284 + # url: http://localhost:8000/items/1/share/
285 + # 마지막 slash도 써주어야함
286 + @csrf_exempt
287 + @action(methods=['POST'], detail=True, permission_classes=[AllowAny], url_path='share', url_name='share')
288 + def share(self, request, pk):
289 + if request.method == 'POST':
290 + password = request.POST.get('password', '')
291 + expires = request.POST.get('expires', '')
292 +
293 + sharedfile = get_object_or_None(SharedItem, item_id=pk)
294 + if sharedfile != None:
295 + # 서버는 정상이나 이미 공유객체로 등록된 파일임
296 + return Response({'message': 'This file is already shared'}, status=status.HTTP_200_OK)
297 + sharedfile = SharedItem(item_id =pk, password=password, expires = expires)
298 + sharedfile.save()
299 + sharedfile = SharedItem.objects.get(item_id = pk)
300 +
301 + # sf = serializers.serialize("json", sharedfile)
302 + item = Item.objects.filter(item_id = pk)
303 + item_json = serializers.serialize("json", item)
304 +
305 + json_data = json.loads(item_json)
306 + print(json_data)
307 + res = json_data[0]['fields']
308 + res['id'] = json_data[0]['pk']
309 + return Response({"shared": sharedfile.created_time , 'data': res}, status=status.HTTP_200_OK)
310 +
311 +item = ItemViewSet.as_view({
312 + 'delete': 'destroy',
313 +})
1 -from django.contrib import admin
2 -
3 -# Register your models here.
1 -from django.db import models
2 -
3 -# Create your models here.
1 -from django.shortcuts import render
2 -
3 -# Create your views here.
1 """ 1 """
2 Django settings for khudrive project. 2 Django settings for khudrive project.
3 3
4 -Generated by 'django-admin startproject' using Django 3.0.6. 4 +Generated by 'django-admin startproject' using Django 3.0.7.
5 5
6 For more information on this file, see 6 For more information on this file, see
7 https://docs.djangoproject.com/en/3.0/topics/settings/ 7 https://docs.djangoproject.com/en/3.0/topics/settings/
...@@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ...@@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 20 # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
21 21
22 # SECURITY WARNING: keep the secret key used in production secret! 22 # SECURITY WARNING: keep the secret key used in production secret!
23 -SECRET_KEY = '3b=a99pdbz*48$9$kh@h3tkb*9w-m3vtf8ngyymdzwpl5$emwn' 23 +SECRET_KEY = ')i0_(*4t7k3=rcqp*_i0u((9zbk8q(2(3tk(%$woji-e-37=o*'
24 24
25 # SECURITY WARNING: don't run with debug turned on in production! 25 # SECURITY WARNING: don't run with debug turned on in production!
26 DEBUG = True 26 DEBUG = True
...@@ -38,6 +38,7 @@ INSTALLED_APPS = [ ...@@ -38,6 +38,7 @@ INSTALLED_APPS = [
38 'django.contrib.messages', 38 'django.contrib.messages',
39 'django.contrib.staticfiles', 39 'django.contrib.staticfiles',
40 'rest_framework', 40 'rest_framework',
41 + 'api.apps.ApiConfig',
41 ] 42 ]
42 43
43 MIDDLEWARE = [ 44 MIDDLEWARE = [
...@@ -73,11 +74,18 @@ WSGI_APPLICATION = 'khudrive.wsgi.application' ...@@ -73,11 +74,18 @@ WSGI_APPLICATION = 'khudrive.wsgi.application'
73 74
74 # Database 75 # Database
75 # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 76 # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
76 -
77 DATABASES = { 77 DATABASES = {
78 + # 'default': {
79 + # 'ENGINE': 'django.db.backends.sqlite3',
80 + # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
81 + # }
78 'default': { 82 'default': {
79 - 'ENGINE': 'django.db.backends.sqlite3', 83 + 'ENGINE': 'django.db.backends.postgresql',
80 - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 84 + 'NAME': 'khuDrive',
85 + 'USER': 'jooheekwon',
86 + 'PASSWORD': '',
87 + 'HOST': 'localhost',
88 + 'PORT': '',
81 } 89 }
82 } 90 }
83 91
......
...@@ -13,9 +13,28 @@ Including another URLconf ...@@ -13,9 +13,28 @@ Including another URLconf
13 1. Import the include() function: from django.urls import include, path 13 1. Import the include() function: from django.urls import include, path
14 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 14 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 """ 15 """
16 +from django.urls import include, path
17 +from rest_framework import routers
16 from django.contrib import admin 18 from django.contrib import admin
17 -from django.urls import path 19 +from api import views
20 +from django.conf.urls import url
18 21
22 +router = routers.DefaultRouter()
23 +router.register(r'users', views.UserViewSet)
24 +router.register(r'items', views.ItemViewSet)
25 +router.register(r'items', views.SharedItemViewSet)
26 +
27 +# Wire up our API using automatic URL routing.
28 +# Additionally, we include login URLs for the browsable API.
19 urlpatterns = [ 29 urlpatterns = [
20 path('admin/', admin.site.urls), 30 path('admin/', admin.site.urls),
31 + path('', include(router.urls)),
32 + url(r'^search/$', views.ItemViewSet.search, name='search'),
33 + url(r'^<int:pk>/share/$', views.SharedItemViewSet.share, name='share'),
34 + url(r'^<int:pk>/move/$', views.ItemViewSet.move, name='move'),
35 + url(r'^<int:pk>/copy/$', views.ItemViewSet.copy, name='copy'),
36 + url(r'^<int:pk>/children/$', views.ItemViewSet.children, name='copy'),
37 + url(r'^signup/$', views.UserViewSet.signup, name='signup'),
38 + url(r'^login/$', views.UserViewSet.login, name='login'),
39 +
21 ] 40 ]
......