diff --git a/facile_backlog/api/__init__.py b/facile_backlog/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/facile_backlog/api/serializers.py b/facile_backlog/api/serializers.py new file mode 100644 index 0000000..162fa2c --- /dev/null +++ b/facile_backlog/api/serializers.py @@ -0,0 +1,46 @@ +from rest_framework import serializers +from rest_framework.reverse import reverse + +from ..backlog.models import Project, Backlog, UserStory + + +class ProjectSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField('_url') + + class Meta: + model = Project + fields = ('id', 'url', 'name', 'code', 'description') + + def _url(self, obj): + return reverse("api_project_detail", args=[obj.pk], + request=self.context['request']) + + +class BacklogSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField('_url') + + class Meta: + model = Backlog + fields = ('id', 'url', 'name', 'description') + + def _url(self, obj): + return reverse("api_backlog_detail", args=[obj.project_id, obj.pk], + request=self.context['request']) + + +class StorySerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField('_url') + create_date = serializers.DateTimeField(read_only=True) + code = serializers.FloatField(read_only=True, source='code') + + class Meta: + model = UserStory + fields = ('id', 'code', 'url', 'as_a', 'i_want_to', 'so_i_can', + 'color', 'comments', + 'acceptances', 'points', 'create_date', 'theme', + 'status') + + def _url(self, obj): + return reverse("api_story_detail", + args=[obj.project_id, obj.backlog_id, obj.pk], + request=self.context['request']) diff --git a/facile_backlog/api/urls.py b/facile_backlog/api/urls.py new file mode 100644 index 0000000..6cbab9d --- /dev/null +++ b/facile_backlog/api/urls.py @@ -0,0 +1,37 @@ +from django.conf.urls import url, patterns, include + +from .views import (home_view, project_list, project_detail, + backlog_list, backlog_detail, story_list, story_detail) + +urlpatterns = patterns( + '', + + url(r'^api-auth/', include('rest_framework.urls', + namespace='rest_framework')), + + url(r'^api-token-auth/', + 'rest_framework.authtoken.views.obtain_auth_token'), + + # views + url(r'^projects/$', + project_list, name="api_project_list"), + + url(r'^projects/(?P[\w]+)/$', + project_detail, name="api_project_detail"), + + url(r'^projects/(?P[\w]+)/backlogs/$', + backlog_list, name="api_backlog_list"), + + url(r'^projects/(?P[\w]+)/backlogs/(?P[\w]+)/$', + backlog_detail, name="api_backlog_detail"), + + url(r'^projects/(?P[\w]+)/backlogs/' + r'(?P[\w]+)/stories/$', + story_list, name="api_story_list"), + + url(r'^projects/(?P[\w]+)/backlogs/(?P[\w]+)/' + r'stories/(?P[\w]+)/$', + story_detail, name="api_story_detail"), + + url(r'^$', home_view, name="api_home"), +) diff --git a/facile_backlog/api/views.py b/facile_backlog/api/views.py new file mode 100644 index 0000000..ef29726 --- /dev/null +++ b/facile_backlog/api/views.py @@ -0,0 +1,104 @@ +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from .serializers import ProjectSerializer, BacklogSerializer, StorySerializer + +from ..backlog.models import Project, Backlog, UserStory + + +class HomeView(viewsets.ViewSet): + _ignore_model_permissions = True + + def list(self, request): + return Response({ + 'projects/': reverse('api_project_list', request=request), + 'projects//': + reverse('api_project_detail', + request=request, + args=("project_id",)), + 'projects//backlogs/': + reverse('api_backlog_list', + request=request, + args=("project_id",)), + 'projects//backlogs//': + reverse('api_backlog_detail', + request=request, + args=("project_id", "backlog_id")), + }) +home_view = HomeView.as_view({'get': 'list'}) + + +class ProjectViewSet(viewsets.ModelViewSet): + pk_url_kwarg = "project_id" + serializer_class = ProjectSerializer + model = Project + + def initial(self, request, *args, **kwargs): + self.queryset = Project.my_projects(request.user) + return super(ProjectViewSet, self).initial(request, *args, **kwargs) + +project_list = ProjectViewSet.as_view({ + 'get': 'list', +}) + +project_detail = ProjectViewSet.as_view({ + 'get': 'retrieve' +}) + + +class BacklogViewSet(viewsets.ModelViewSet): + pk_url_kwarg = "backlog_id" + serializer_class = BacklogSerializer + model = Backlog + + def initial(self, request, *args, **kwargs): + project_id = kwargs.pop("project_id") + projects = Project.my_projects(request.user) + try: + self.project = projects.get(pk=project_id) + self.queryset = self.project.backlogs.all() + except Project.DoesNotExist: + self.project = None + self.queryset = Backlog.objects.none() + return super(BacklogViewSet, self).initial(request, *args, **kwargs) + +backlog_list = BacklogViewSet.as_view({ + 'get': 'list' +}) + +backlog_detail = BacklogViewSet.as_view({ + 'get': 'retrieve' +}) + + +class StoryViewSet(viewsets.ModelViewSet): + pk_url_kwarg = "story_id" + serializer_class = StorySerializer + model = UserStory + + def initial(self, request, *args, **kwargs): + project_id = kwargs.pop("project_id") + backlog_id = kwargs.pop("backlog_id") + projects = Project.my_projects(request.user) + try: + self.project = projects.get(pk=project_id) + self.backlog = self.project.backlogs.get(pk=backlog_id) + self.queryset = self.backlog.ordered_stories + except Project.DoesNotExist, Backlog.DoesNotExist: + self.project = None + self.backlog = None + self.queryset = UserStory.objects.none() + return super(StoryViewSet, self).initial(request, *args, **kwargs) + +story_list = StoryViewSet.as_view({ + 'get': 'list', + 'post': 'create' +}) + +story_detail = StoryViewSet.as_view({ + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' +}) diff --git a/facile_backlog/backlog/admin.py b/facile_backlog/backlog/admin.py index 83b76c8..47d8436 100644 --- a/facile_backlog/backlog/admin.py +++ b/facile_backlog/backlog/admin.py @@ -30,7 +30,8 @@ class EventAdmin(admin.ModelAdmin): def get_readonly_fields(self, request, obj=None): if obj: - self.readonly_fields = [field.name for field in obj.__class__._meta.fields] + self.readonly_fields = [ + field.name for field in obj.__class__._meta.fields] return self.readonly_fields return self.readonly_fields @@ -47,4 +48,3 @@ def queryset(self, request): admin.site.register(UserStory, UserStoryAdmin) admin.site.register(AuthorizationAssociation, AuthorizationAssociationAdmin) admin.site.register(Event, EventAdmin) - diff --git a/facile_backlog/backlog/forms.py b/facile_backlog/backlog/forms.py index af4ed4e..87abe76 100644 --- a/facile_backlog/backlog/forms.py +++ b/facile_backlog/backlog/forms.py @@ -1,7 +1,7 @@ from django.forms.fields import CharField from django.forms.models import ModelForm from django.forms import Form -from django.forms import EmailField, BooleanField, ChoiceField, CharField +from django.forms import EmailField, BooleanField, ChoiceField from django.utils.translation import ugettext as _ from .models import Project, Backlog, UserStory diff --git a/facile_backlog/backlog/models.py b/facile_backlog/backlog/models.py index 8e636a1..857b0a1 100644 --- a/facile_backlog/backlog/models.py +++ b/facile_backlog/backlog/models.py @@ -99,6 +99,8 @@ def __unicode__(self): @classmethod def my_projects(cls, user): """ Return all project user accepted the invitation """ + if not user.is_authenticated(): + return Project.objects.none() return user.projects.filter( authorizationassociation__is_active=True ) @@ -381,7 +383,7 @@ def build_event_kwargs(values, **kwargs): return values -def create_event(user, project, text, backlog=None, story=None ): +def create_event(user, project, text, backlog=None, story=None): kwargs = { 'text': text } diff --git a/facile_backlog/backlog/views.py b/facile_backlog/backlog/views.py index 8e581a5..07c72d9 100644 --- a/facile_backlog/backlog/views.py +++ b/facile_backlog/backlog/views.py @@ -44,7 +44,7 @@ class ProjectList(generic.ListView): paginate_by = 10 def dispatch(self, request, *args, **kwargs): - self.query = request.GET.get("q","").replace("+", " ") + self.query = request.GET.get("q", "").replace("+", " ") return super(ProjectList, self).dispatch(request, *args, **kwargs) def get_queryset(self): diff --git a/facile_backlog/core/admin.py b/facile_backlog/core/admin.py index 17f637d..803ead5 100644 --- a/facile_backlog/core/admin.py +++ b/facile_backlog/core/admin.py @@ -25,7 +25,7 @@ class UserAdmin(UserAdmin): form = UserChangeForm add_form = UserCreationForm list_display = ['email', 'full_name', 'is_active', 'is_staff', - 'is_superuser'] + 'is_superuser', 'auth_token'] list_filter = ['is_staff', 'is_superuser', 'is_active'] search_fields = ['email', 'full_name'] ordering = ['email'] diff --git a/facile_backlog/core/forms.py b/facile_backlog/core/forms.py index 2322691..9f3e3db 100644 --- a/facile_backlog/core/forms.py +++ b/facile_backlog/core/forms.py @@ -4,6 +4,8 @@ from django.core.validators import validate_email from django.utils.translation import ugettext_lazy as _ +from rest_framework.authtoken.models import Token + from password_reset.forms import (PasswordRecoveryForm as BaseRecovery, PasswordResetForm as BaseReset) @@ -97,3 +99,17 @@ class PasswordResetForm(BaseReset): def save(self): self.user.set_password(self.cleaned_data['password1']) self.user.save(update_fields=['password']) + + +class ChangeApiKeyForm(forms.Form): + + def __init__(self, user, *args, **kwargs): + super(ChangeApiKeyForm, self).__init__(*args, **kwargs) + self.user = user + + def change_or_create(self): + token, created = Token.objects.get_or_create(user=self.user) + if token: + token.delete() + Token.objects.get_or_create(user=self.user) + return created diff --git a/facile_backlog/core/static/core/css/main.css b/facile_backlog/core/static/core/css/main.css index 026b1e9..91cc0b7 100644 --- a/facile_backlog/core/static/core/css/main.css +++ b/facile_backlog/core/static/core/css/main.css @@ -1,4 +1,4 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block}html,body{font-family:OpenSansCondensedBold, 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#333;background:#eee;height:100%}h1{font-size:1.5em;line-height:1.5em;margin:0.5rem 0}h2{font-size:1.25em;line-height:1.25em;color:gray;margin:0.25rem 0}b,strong{font-weight:bold}i,italic{font-style:italic}p{margin-bottom:0.25rem}#header{overflow:hidden;*zoom:1;background-color:#0f787e;background-image:-webkit-linear-gradient(top, #0f787e 0%, #0c6267 100%);background-image:-moz-linear-gradient(top, #0f787e 0%, #0c6267 100%);background-image:linear-gradient(top, #0f787e 0%,#0c6267 100%);color:#fbffdb;height:3em;box-shadow:0 1px 5px rgba(0,0,0,0.5);border-top:1px solid black;border-bottom:1px solid rgba(0,0,0,0.3)}#header .header-in{width:900px;margin:0 auto}#header .avatar{min-height:32px}#header .title{font-size:1.25em;float:left;padding:0.5em;width:50px;text-shadow:0px 0px 10px rgba(255,255,255,0.5)}#header a{color:#fbffdb;text-decoration:none}#header a:hover{color:#fff}#header a:before{padding-right:0.5em}#header .identity{float:right;margin-top:9px}#header .identity a *{vertical-align:middle}#header .buttons{margin-top:3px;display:block;padding:0.5em;float:right}#header .buttons>*{display:inline-block}#header .buttons a{font-weight:normal}#header .buttons inpurt{font-family:inherit}#header .buttons .sep{padding-right:0.5em;text-shadow:-1px 0 1px black;border-left:1px solid #fbffdb;width:1px;height:1em;margin-left:0.5em}#header .logout{padding:0;color:#fbffdb;background:none;border-width:0;font-size:1em;cursor:pointer}#header .logout:hover{color:#fff}#header .notif-link:hover .notification_count{background-color:rgba(255,0,0,0.8)}#header .notification_count{border:1px solid rgba(0,0,0,0.3);background-color:rgba(255,0,0,0.5);box-shadow:1px 1px 1px rgba(255,255,255,0.3);border-radius:2em;padding:0.25em 0.5em;box-sizing:border-box;display:inline-block}#content{overflow:hidden;*zoom:1;width:900px;margin:0 auto;padding:0 0.25em;overflow:visible}#content #messages{background-color:#073a3d}#content #messages>div{padding:0.5em;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.1)}#content #messages .success{color:#CFC}#content #messages .success:before{font-family:FontAwesome;content:"\f00c";padding-right:0.5em}#content #messages .error{color:#FCC}#content #messages .error:before{font-family:FontAwesome;content:"\f071";padding-right:0.5em}#content .path{display:block;font-size:1rem;padding:0.5rem 1rem;font-weight:normal;background-color:#0c6267;border-top:1px solid rgba(255,255,255,0.2);border-bottom:1px solid rgba(0,0,0,0.3);box-shadow:0px -2px 5px rgba(0,0,0,0.2);border-radius:0 0 0rem 0rem}#content .path i{padding-right:0.25rem;color:#e6efef}#content .path .sep{padding-left:0.25rem;color:black;text-shadow:1px 1px 1px rgba(255,255,255,0.2)}#content .path a{color:#e6efef;text-decoration:none}#content .path a:hover{color:#fbffdb}#content .path .current{color:#fbffdb}#content .path .action{font-style:italic}#content .sub-field{color:#737373;font-size:0.75rem;padding:0.25rem;font-weight:bold}#header a,#links a{color:#fbffdb;text-decoration:none}#header a:hover,#links a:hover{color:#fff}#header a:before,#links a:before{padding-right:0.5em}form .field{font-size:1.25rem;margin:0.5rem}form .field label{display:inline-block;width:200px;text-align:right;vertical-align:top;padding:0.25rem}form .field input,form .field textarea{font-size:1em;width:500px}form .field .helptext{color:#aaa;font-size:0.75em}form .field .errorlist,form .field .helptext{width:500px;padding:0.25em 0.25em;margin:0 0 10px 214px}form .submit{padding-left:220px}form .submit input{font-size:1rem}.left{float:left;width:450px}.right{margin-left:1em;float:right;width:350px}.events-list{font-size:0.75em;line-height:1rem;border-collapse:collapse}.events-list td{vertical-align:top;padding-bottom:10px}.events-list img{margin-right:6px;width:32px;height:32px}.events-list .info a{border-left:2px solid #0f787e;color:#0f787e;padding-left:6px;margin-left:4px}.events-list .when{float:left;line-height:2em}.events-list .when{color:#999}.project-list li,.backlog-list li{display:block;font-size:1.5em;min-height:100px;overflow:hidden;margin-bottom:1em}.project-list li .title,.backlog-list li .title{border-bottom:2px solid #0f787e;padding:0.2em 0}.project-list li .title .black,.backlog-list li .title .black{color:#333}.project-list li .title a,.backlog-list li .title a{color:#0f787e;text-decoration:none}.project-list li .title a:hover,.backlog-list li .title a:hover{color:initial}.project-list li .info,.backlog-list li .info{color:#999}.project-list li .stats,.backlog-list li .stats{margin-left:2rem}.project-list li .create,.backlog-list li .create{font-size:0.75rem}.project-list .story-stats,.backlog-list .story-stats{float:left;display:inline-block;width:300px;margin:0.25em;background-color:rgba(0,0,0,0.05);height:59px}.project-list .top-stories,.backlog-list .top-stories{float:left;display:inline-block;font-size:1rem;padding-left:0.25em;margin:0.25em}.project-list .top-stories .story,.backlog-list .top-stories .story{display:inline-block;padding:2px 0}.button{font-size:1em;line-height:1em;font-weight:normal;display:inline-block !important;white-space:nowrap;text-decoration:none;text-shadow:1px 1px 0 rgba(0,0,0,0.2);cursor:pointer;border:1px solid rgba(0,0,0,0.2);background-color:#ff5900;background-image:-webkit-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:-moz-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:-ms-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:-o-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:linear-gradient(top, #ff5900 50%,#d64b00 55%);color:#fbffdb;padding:0.5rem 0.5rem;box-shadow:0 0 1px 1px rgba(255,255,255,0.4) inset,0 1px 0 rgba(0,0,0,0.2);border-radius:0.25rem}.button:hover{background-color:#ff7a33;background-image:none;color:#fff}.button:before{padding-right:0.2rem}.alt-button{cursor:pointer;border:none;background:none;font-size:1em;font-family:inherit;padding:0 0.25em;color:#ff5900;font-weight:bold;text-decoration:none}.alt-button:hover{text-decoration:underline}.project-count{padding:0.5rem;font-weight:bold;background-color:white;border-bottom:2px solid rgba(0,0,0,0.3);display:block}.description{margin:0.5em 0;line-height:1.2em;color:#999}.actions{padding:1em 0;border-top:1px solid rgba(0,0,0,0.3)}p.errorlist{background-color:#ffb3b3;padding:0.5rem;border:1px solid red}ul.errorlist li{color:red;font-size:1rem;margin:0.25em 0}.markdown ul{margin-left:1rem;list-style-position:inside;list-style-type:circle}.markdown ul li{margin:0.2rem 0}.acceptance-criteria ul{list-style-type:lower-alpha !important;list-style-position:outside !important}.acceptance-criteria ul li{background-color:white;box-shadow:1px 1px 3px rgba(0,0,0,0.2);margin:0.5em;padding:0.5em}.story-count,.kind,.points-count{font-size:1rem;font-family:monospace;padding:0.25em;margin:0.25em;display:inline-block;border:1px solid rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.05);color:gray}article.story{display:block;position:relative;margin:0.5em 0 0.5em 0;padding:1px}article.story .handle{position:absolute;top:1px;width:10px;bottom:1px;left:0px;background-image:url("../img/grab.png");background-color:rgba(127,127,127,0.5);border-top:1px solid rgba(0,0,0,0.2);border-left:1px solid rgba(0,0,0,0.2);border-bottom:1px solid rgba(0,0,0,0.2);cursor:pointer}article.story .handled{padding-left:15px;background-color:rgba(0,0,0,0.1);border-top:1px solid rgba(0,0,0,0.2);border-right:1px solid rgba(0,0,0,0.2);border-bottom:1px solid rgba(0,0,0,0.2);border-left:1px solid rgba(255,255,255,0.6);padding:8px 2px 8px 18px}article.story .color{content:"\00a0";width:1em;float:left;font-size:1.25rem;line-height:1.25rem;border:1px solid rgba(0,0,0,0.2);margin:0.5em 0.25em 0 0}article.story .code{display:inline-block;width:160px;float:left;font-size:1.25rem;margin:0.5em 0}article.story .code a{color:#0f787e;font-weight:bold;text-decoration:none}article.story .code a:hover{color:#333}article.story .text{display:inline-block;margin:0.5em 0;width:600px;min-height:2em;line-height:1.25em}article.story .points{display:inline-block;width:40px;height:40px;line-height:36px;text-align:center;vertical-align:middle;border-radius:2rem;float:right;margin:0.25em;font-size:1.5rem;background-color:#333;color:#eee;width:40px}article.story .theme{display:inline-block;color:gray;padding:0.25rem}article.story .theme i{padding-right:0.25rem}article.story .actions-button{float:right;display:block;box-sizing:border-box}article.story .dropdown-button{display:inline-block;margin:0.25em;border:1px solid rgba(0,0,0,0.2);color:#333;padding:0.25em}article.story .dropdown-button:hover{background-color:rgba(255,255,255,0.5) !important}article.story .status-button{display:inline-block;margin:0.25em;border:1px solid rgba(0,0,0,0.2);color:#333;padding:0.25em}article.story .status-button:hover{background-color:rgba(255,255,255,0.5)}article.story .no-points{background-color:#909090}article.story.highlighted{box-shadow:0px 0px 10px green}article.story.opacified{opacity:0.3}article.story .status{width:152px;padding-left:1em;margin-left:0}article.story .status[status=in_progress]{background-color:#d6d6ef}article.story .status[status=accepted]{background-color:#d6e3d6}article.story .status[status=rejected]{background-color:#efd6d6}article.story .status i{padding-left:1em;float:right}.handle-highlight{background-color:rgba(255,255,255,0.5);border:1px dashed rgba(0,0,0,0.2);margin:0.5em 0 0.5em 0;padding:-1px}.story-detail .points{display:table-cell;float:right;width:60px;height:60px;line-height:56px;text-align:center;vertical-align:middle;font-size:2em;margin:0 0.5em;background-color:#333;color:#eee;padding:0;box-shadow:3px 3px 18px rgba(0,0,0,0.5);border-radius:2rem}.story-detail .no-points{background-color:#dbdbdb}.story-detail .description{font-size:1.5em;line-height:1.5em;display:block;width:700px;background:white;padding:1em;box-shadow:3px 3px 18px rgba(0,0,0,0.5);border-radius:0.25em;color:#333}.story-detail .theme{color:gray;padding:0.25rem}.story-detail .theme i{padding-right:0.25rem}.poutre{border-bottom:1px solid #bbbbbb;box-shadow:0 1px 1px white;padding:0.5em 0.25em}.dropdown-button{box-sizing:border-box}.dropdown-button>li>a{color:#333;text-decoration:none}.dropdown-button>li>a:hover{color:#000}.dropdown-button ul{display:none}.dropdown-button ul.dropit-submenu{background-color:#fff;border:1px solid #b2b2b2;padding:6px 0;margin:3px 0 0 1px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.15);-moz-box-shadow:0px 1px 3px rgba(0,0,0,0.15);box-shadow:0px 1px 3px rgba(0,0,0,0.15)}.dropdown-button ul.dropit-submenu a{display:block;font-size:14px;line-height:25px;color:#7a868e;padding:0 18px;white-space:nowrap}.dropdown-button ul.dropit-submenu a:hover{background:#248fc1;color:#fff;text-decoration:none}.project-users,.project-stories{width:100%}.project-users a,.project-stories a{text-decoration:none;color:#0f787e;font-weight:normal}.project-users a.active,.project-stories a.active{font-weight:bold}.project-users a:hover,.project-stories a:hover{text-decoration:underline}.project-users thead tr,.project-stories thead tr{background-color:#fdfdfd;border-bottom:2px solid #abcacc}.project-users thead td,.project-stories thead td{padding:0.25em;font-weight:bold;white-space:nowrap}.project-users tbody tr:nth-child(even),.project-stories tbody tr:nth-child(even){background-color:#d7e2e2}.project-users tbody .user-image,.project-stories tbody .user-image{min-height:28px;min-width:28px}.project-users tbody td,.project-stories tbody td{padding:0.25em}.project-users tbody .user-actions a,.project-stories tbody .user-actions a{text-decoration:none;color:#f67777;font-size:1.5em}.project-users tbody .user-actions a:hover,.project-stories tbody .user-actions a:hover{color:#fb2f2f}.project-users tbody [status],.project-stories tbody [status]{font-size:0.75em;white-space:nowrap;padding:0.25em 0.5em;background-color:gray;color:white}.project-users tbody [status][status=accepted],.project-stories tbody [status][status=accepted]{background-color:#00b432}.project-users tbody [status][status=rejected],.project-stories tbody [status][status=rejected]{background-color:#c80032}.project-users tbody [status][status=completed],.project-stories tbody [status][status=completed]{background-color:#006496}.project-users tbody .story-theme,.project-stories tbody .story-theme{color:gray;font-size:0.75em}.project-users .story-code,.project-stories .story-code{width:100px;font-weight:bold}.project-users .story-desc,.project-stories .story-desc{padding:0.5em}#links{background-color:#0c6267;border-top:1px solid rgba(255,255,255,0.2);border-bottom:1px solid rgba(0,0,0,0.5);padding:1em 0.5em 0em 0.5em}#links a{padding:0.25em 0.5em;display:inline-block}#links .tab{padding:0.1em 0.5em 7px 0.5em;margin:0 0.5em;background-color:#0d676c;box-shadow:0 0 1px 1px rgba(255,255,255,0.1) inset,0 1px 2px rgba(0,0,0,0.5);border:1px solid rgba(0,0,0,0.2)}#links .tab.active{padding:0.1em 1em 7px 1em;background-color:#eee;border-top:1px solid rgba(0,0,0,0.5);border-bottom:0 solid transparent;border-left:0 solid transparent;border-right:0 solid transparent;box-shadow:none;font-weight:bold}.home-login{width:300px;float:right;clear:right;background-color:rgba(255,255,255,0.3);border-top:1px solid rgba(255,255,255,0.3);border-left:1px solid rgba(255,255,255,0.3);border-radius:0.25em;box-shadow:3px 3px 6px rgba(0,0,0,0.2);padding:1em;margin:1em}.home-login input{font-size:1.2em;font-size:1.2em;margin:6px 0}.home-login .full{width:100%;margin-bottom:0.25em}.home-login #id_password{width:60%}.home-login .button{font-size:1em;width:30%;max-width:30%;float:right}.home-login h3{font-weight:bold;padding:0 0 0.5em 0}.home-login h1{font-size:4rem}input[colorpicker=true]{width:6em !important}.login-background,.home-background{background-image:url("../img/background.jpg");background-color:1}.login-background h1,.login-background label,.home-background h1,.home-background label{text-shadow:0px 0px 12px rgba(251,255,219,0.7);color:#fbffdb;font-size:2em}.login-background p,.home-background p{color:rgba(255,255,255,0.5)}.login-background a,.home-background a{color:black;text-shadow:1px 1px 1px rgba(255,255,255,0.3)}.login-background .alert,.home-background .alert{float:left;width:500px}.login-background .alert span,.home-background .alert span{margin-top:1em;width:1em;display:inline-block;font-size:3em;color:rgba(0,0,0,0.3);text-shadow:1px 1px 1px rgba(255,255,255,0.1)}.invitation{margin:0.5em;box-sizing:border-box;border:1px solid rgba(0,0,0,0.1)}.invitation p{padding:0.5em}.invitation .invit-actions{background-color:rgba(0,0,0,0.1);padding:0.5em}.invitation .invit-actions form{display:inline-block}textarea[name=as_a]{height:3rem}textarea[name=so_i_can]{height:6rem}textarea[name=i_want_to]{height:6rem}.percent-cell{font-size:1rem;border:1px solid rgba(0,0,0,0.3);box-sizing:border-box}.percent-cell .name{display:block;width:100%;clear:both;padding:0.25em}.percent-cell .percent{font-size:0.75em;color:rgba(0,0,0,0.5);text-align:left;line-height:1.25em;padding-left:0.25em;clear:both;display:block;height:1rem;min-height:1rem;background-color:#eee;border:1px solid rgba(0,0,0,0.2);box-sizing:border-box;overflow-x:visible;white-space:nowrap}.legend{border:1px solid rgba(0,0,0,0.2);font-size:0.75rem;text-align:left;line-height:1.25em;padding:0.25em}.bg-green{background-color:#c8ffe6 !important}.bg-blue{background-color:#c8e6ff !important}.search input{font-size:1rem}.project-head .name{display:inline-block;font-size:1.5em;margin:0.5em 0}.project-head a{font-size:0.75em;vertical-align:text-top}/* +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary{display:block}html,body{font-family:OpenSansCondensedBold, 'Helvetica Neue', Helvetica, Arial, sans-serif;color:#333;background:#eee;height:100%}h1{font-size:1.5em;line-height:1.5em;margin:0.5rem 0}h2{font-size:1.25em;line-height:1.25em;color:gray;margin:0.25rem 0}b,strong{font-weight:bold}i,italic{font-style:italic}p{margin-bottom:0.25rem}#header{overflow:hidden;*zoom:1;background-color:#0f787e;background-image:-webkit-linear-gradient(top, #0f787e 0%, #0c6267 100%);background-image:-moz-linear-gradient(top, #0f787e 0%, #0c6267 100%);background-image:linear-gradient(top, #0f787e 0%,#0c6267 100%);color:#fbffdb;height:3em;box-shadow:0 1px 5px rgba(0,0,0,0.5);border-top:1px solid black;border-bottom:1px solid rgba(0,0,0,0.3)}#header .header-in{width:900px;margin:0 auto}#header .avatar{min-height:32px}#header .title{font-size:1.25em;float:left;padding:0.5em;width:50px;text-shadow:0px 0px 10px rgba(255,255,255,0.5)}#header a{color:#fbffdb;text-decoration:none}#header a:hover{color:#fff}#header a:before{padding-right:0.5em}#header .identity{float:right;margin-top:9px}#header .identity a *{vertical-align:middle}#header .buttons{margin-top:3px;display:block;padding:0.5em;float:right}#header .buttons>*{display:inline-block}#header .buttons a{font-weight:normal}#header .buttons inpurt{font-family:inherit}#header .buttons .sep{padding-right:0.5em;text-shadow:-1px 0 1px black;border-left:1px solid #fbffdb;width:1px;height:1em;margin-left:0.5em}#header .logout{padding:0;color:#fbffdb;background:none;border-width:0;font-size:1em;cursor:pointer}#header .logout:hover{color:#fff}#header .notif-link:hover .notification_count{background-color:rgba(255,0,0,0.8)}#header .notification_count{border:1px solid rgba(0,0,0,0.3);background-color:rgba(255,0,0,0.5);box-shadow:1px 1px 1px rgba(255,255,255,0.3);border-radius:2em;padding:0.25em 0.5em;box-sizing:border-box;display:inline-block}#content{overflow:hidden;*zoom:1;width:900px;margin:0 auto;padding:0 0.25em;overflow:visible}#content #messages{background-color:#073a3d}#content #messages>div{padding:0.5em;border-top:1px solid rgba(0,0,0,0.1);border-bottom:1px solid rgba(255,255,255,0.1)}#content #messages .success{color:#CFC}#content #messages .success:before{font-family:FontAwesome;content:"\f00c";padding-right:0.5em}#content #messages .error{color:#FCC}#content #messages .error:before{font-family:FontAwesome;content:"\f071";padding-right:0.5em}#content .path{display:block;font-size:1rem;padding:0.5rem 1rem;font-weight:normal;background-color:#0c6267;border-top:1px solid rgba(255,255,255,0.2);border-bottom:1px solid rgba(0,0,0,0.3);box-shadow:0px -2px 5px rgba(0,0,0,0.2);border-radius:0 0 0rem 0rem}#content .path i{padding-right:0.25rem;color:#e6efef}#content .path .sep{padding-left:0.25rem;color:black;text-shadow:1px 1px 1px rgba(255,255,255,0.2)}#content .path a{color:#e6efef;text-decoration:none}#content .path a:hover{color:#fbffdb}#content .path .current{color:#fbffdb}#content .path .action{font-style:italic}#content .sub-field{color:#737373;font-size:0.75rem;padding:0.25rem;font-weight:bold}#header a,#links a{color:#fbffdb;text-decoration:none}#header a:hover,#links a:hover{color:#fff}#header a:before,#links a:before{padding-right:0.5em}form .field{font-size:1.25rem;margin:0.5rem}form .field label{display:inline-block;width:200px;text-align:right;vertical-align:top;padding:0.25rem}form .field input,form .field textarea{font-size:1em;width:500px}form .field .helptext{color:#aaa;font-size:0.75em}form .field .errorlist,form .field .helptext{width:500px;padding:0.25em 0.25em;margin:0 0 10px 214px}form .read-only-field{vertical-align:sub;color:#999}form pre{font-size:1.2em;font-family:monospace;display:inline-block;color:#333;background-color:white;border:1px dashed rgba(0,0,0,0.2);padding:2px}form .submit{padding-left:220px}form .submit input{font-size:1rem}.left{float:left;width:450px}.right{margin-left:1em;float:right;width:350px}.events-list{font-size:0.75em;line-height:1rem;border-collapse:collapse}.events-list td{vertical-align:top;padding-bottom:10px}.events-list img{margin-right:6px;width:32px;height:32px}.events-list .info a{border-left:2px solid #0f787e;color:#0f787e;padding-left:6px;margin-left:4px}.events-list .when{float:left;line-height:2em}.events-list .when{color:#999}.project-list li,.backlog-list li{display:block;font-size:1.5em;min-height:100px;overflow:hidden;margin-bottom:1em}.project-list li .title,.backlog-list li .title{border-bottom:2px solid #0f787e;padding:0.2em 0}.project-list li .title .black,.backlog-list li .title .black{color:#333}.project-list li .title a,.backlog-list li .title a{color:#0f787e;text-decoration:none}.project-list li .title a:hover,.backlog-list li .title a:hover{color:initial}.project-list li .info,.backlog-list li .info{color:#999}.project-list li .stats,.backlog-list li .stats{margin-left:2rem}.project-list li .create,.backlog-list li .create{font-size:0.75rem}.project-list .story-stats,.backlog-list .story-stats{float:left;display:inline-block;width:300px;margin:0.25em;background-color:rgba(0,0,0,0.05);height:59px}.project-list .top-stories,.backlog-list .top-stories{float:left;display:inline-block;font-size:1rem;padding-left:0.25em;margin:0.25em}.project-list .top-stories .story,.backlog-list .top-stories .story{display:inline-block;padding:2px 0}.button{font-size:1em;line-height:1em;font-weight:normal;display:inline-block !important;white-space:nowrap;text-decoration:none;text-shadow:1px 1px 0 rgba(0,0,0,0.2);cursor:pointer;border:1px solid rgba(0,0,0,0.2);background-color:#ff5900;background-image:-webkit-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:-moz-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:-ms-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:-o-linear-gradient(top, #ff5900 50%, #d64b00 55%);background-image:linear-gradient(top, #ff5900 50%,#d64b00 55%);color:#fbffdb;padding:0.5rem 0.5rem;box-shadow:0 0 1px 1px rgba(255,255,255,0.4) inset,0 1px 0 rgba(0,0,0,0.2);border-radius:0.25rem}.button:hover{background-color:#ff7a33;background-image:none;color:#fff}.button:before{padding-right:0.2rem}.alt-button{cursor:pointer;border:none;background:none;font-size:1em;font-family:inherit;padding:0 0.25em;color:#ff5900;font-weight:bold;text-decoration:none}.alt-button:hover{text-decoration:underline}.project-count{padding:0.5rem;font-weight:bold;background-color:white;border-bottom:2px solid rgba(0,0,0,0.3);display:block}.description{margin:0.5em 0;line-height:1.2em;color:#999}.actions{padding:1em 0;border-top:1px solid rgba(0,0,0,0.3)}p.errorlist{background-color:#ffb3b3;padding:0.5rem;border:1px solid red}ul.errorlist li{color:red;font-size:1rem;margin:0.25em 0}.markdown ul{margin-left:1rem;list-style-position:inside;list-style-type:circle}.markdown ul li{margin:0.2rem 0}.acceptance-criteria ul{list-style-type:lower-alpha !important;list-style-position:outside !important}.acceptance-criteria ul li{background-color:white;box-shadow:1px 1px 3px rgba(0,0,0,0.2);margin:0.5em;padding:0.5em}.story-count,.kind,.points-count{font-size:1rem;font-family:monospace;padding:0.25em;margin:0.25em;display:inline-block;border:1px solid rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.05);color:gray}article.story{display:block;position:relative;margin:0.5em 0 0.5em 0;padding:1px}article.story .handle{position:absolute;top:1px;width:10px;bottom:1px;left:0px;background-image:url("../img/grab.png");background-color:rgba(127,127,127,0.5);border-top:1px solid rgba(0,0,0,0.2);border-left:1px solid rgba(0,0,0,0.2);border-bottom:1px solid rgba(0,0,0,0.2);cursor:pointer}article.story .handled{padding-left:15px;background-color:rgba(0,0,0,0.1);border-top:1px solid rgba(0,0,0,0.2);border-right:1px solid rgba(0,0,0,0.2);border-bottom:1px solid rgba(0,0,0,0.2);border-left:1px solid rgba(255,255,255,0.6);padding:8px 2px 8px 18px}article.story .color{content:"\00a0";width:1em;float:left;font-size:1.25rem;line-height:1.25rem;border:1px solid rgba(0,0,0,0.2);margin:0.5em 0.25em 0 0}article.story .code{display:inline-block;width:160px;float:left;font-size:1.25rem;margin:0.5em 0}article.story .code a{color:#0f787e;font-weight:bold;text-decoration:none}article.story .code a:hover{color:#333}article.story .text{display:inline-block;margin:0.5em 0;width:600px;min-height:2em;line-height:1.25em}article.story .points{display:inline-block;width:40px;height:40px;line-height:36px;text-align:center;vertical-align:middle;border-radius:2rem;float:right;margin:0.25em;font-size:1.5rem;background-color:#333;color:#eee;width:40px}article.story .theme{display:inline-block;color:gray;padding:0.25rem}article.story .theme i{padding-right:0.25rem}article.story .actions-button{float:right;display:block;box-sizing:border-box}article.story .dropdown-button{display:inline-block;margin:0.25em;border:1px solid rgba(0,0,0,0.2);color:#333;padding:0.25em}article.story .dropdown-button:hover{background-color:rgba(255,255,255,0.5) !important}article.story .status-button{display:inline-block;margin:0.25em;border:1px solid rgba(0,0,0,0.2);color:#333;padding:0.25em}article.story .status-button:hover{background-color:rgba(255,255,255,0.5)}article.story .no-points{background-color:#909090}article.story.highlighted{box-shadow:0px 0px 10px green}article.story.opacified{opacity:0.3}article.story .status{width:152px;padding-left:1em;margin-left:0}article.story .status[status=in_progress]{background-color:#d6d6ef}article.story .status[status=accepted]{background-color:#d6e3d6}article.story .status[status=rejected]{background-color:#efd6d6}article.story .status i{padding-left:1em;float:right}.handle-highlight{background-color:rgba(255,255,255,0.5);border:1px dashed rgba(0,0,0,0.2);margin:0.5em 0 0.5em 0;padding:-1px}.story-detail .points{display:table-cell;float:right;width:60px;height:60px;line-height:56px;text-align:center;vertical-align:middle;font-size:2em;margin:0 0.5em;background-color:#333;color:#eee;padding:0;box-shadow:3px 3px 18px rgba(0,0,0,0.5);border-radius:2rem}.story-detail .no-points{background-color:#dbdbdb}.story-detail .description{font-size:1.5em;line-height:1.5em;display:block;width:700px;background:white;padding:1em;box-shadow:3px 3px 18px rgba(0,0,0,0.5);border-radius:0.25em;color:#333}.story-detail .theme{color:gray;padding:0.25rem}.story-detail .theme i{padding-right:0.25rem}.poutre{border-bottom:1px solid #bbbbbb;box-shadow:0 1px 1px white;padding:0.5em 0.25em}.dropdown-button{box-sizing:border-box}.dropdown-button>li>a{color:#333;text-decoration:none}.dropdown-button>li>a:hover{color:#000}.dropdown-button ul{display:none}.dropdown-button ul.dropit-submenu{background-color:#fff;border:1px solid #b2b2b2;padding:6px 0;margin:3px 0 0 1px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.15);-moz-box-shadow:0px 1px 3px rgba(0,0,0,0.15);box-shadow:0px 1px 3px rgba(0,0,0,0.15)}.dropdown-button ul.dropit-submenu a{display:block;font-size:14px;line-height:25px;color:#7a868e;padding:0 18px;white-space:nowrap}.dropdown-button ul.dropit-submenu a:hover{background:#248fc1;color:#fff;text-decoration:none}.project-users,.project-stories{width:100%}.project-users a,.project-stories a{text-decoration:none;color:#0f787e;font-weight:normal}.project-users a.active,.project-stories a.active{font-weight:bold}.project-users a:hover,.project-stories a:hover{text-decoration:underline}.project-users thead tr,.project-stories thead tr{background-color:#fdfdfd;border-bottom:2px solid #abcacc}.project-users thead td,.project-stories thead td{padding:0.25em;font-weight:bold;white-space:nowrap}.project-users tbody tr:nth-child(even),.project-stories tbody tr:nth-child(even){background-color:#d7e2e2}.project-users tbody .user-image,.project-stories tbody .user-image{min-height:28px;min-width:28px}.project-users tbody td,.project-stories tbody td{padding:0.25em}.project-users tbody .user-actions a,.project-stories tbody .user-actions a{text-decoration:none;color:#f67777;font-size:1.5em}.project-users tbody .user-actions a:hover,.project-stories tbody .user-actions a:hover{color:#fb2f2f}.project-users tbody [status],.project-stories tbody [status]{font-size:0.75em;white-space:nowrap;padding:0.25em 0.5em;background-color:gray;color:white}.project-users tbody [status][status=accepted],.project-stories tbody [status][status=accepted]{background-color:#00b432}.project-users tbody [status][status=rejected],.project-stories tbody [status][status=rejected]{background-color:#c80032}.project-users tbody [status][status=completed],.project-stories tbody [status][status=completed]{background-color:#006496}.project-users tbody .story-theme,.project-stories tbody .story-theme{color:gray;font-size:0.75em}.project-users .story-code,.project-stories .story-code{width:100px;font-weight:bold}.project-users .story-desc,.project-stories .story-desc{padding:0.5em}#links{background-color:#0c6267;border-top:1px solid rgba(255,255,255,0.2);border-bottom:1px solid rgba(0,0,0,0.5);padding:1em 0.5em 0em 0.5em}#links a{padding:0.25em 0.5em;display:inline-block}#links .tab{padding:0.1em 0.5em 7px 0.5em;margin:0 0.5em;background-color:#0d676c;box-shadow:0 0 1px 1px rgba(255,255,255,0.1) inset,0 1px 2px rgba(0,0,0,0.5);border:1px solid rgba(0,0,0,0.2)}#links .tab.active{padding:0.1em 1em 7px 1em;background-color:#eee;border-top:1px solid rgba(0,0,0,0.5);border-bottom:0 solid transparent;border-left:0 solid transparent;border-right:0 solid transparent;box-shadow:none;font-weight:bold}.home-login{width:300px;float:right;clear:right;background-color:rgba(255,255,255,0.3);border-top:1px solid rgba(255,255,255,0.3);border-left:1px solid rgba(255,255,255,0.3);border-radius:0.25em;box-shadow:3px 3px 6px rgba(0,0,0,0.2);padding:1em;margin:1em}.home-login input{font-size:1.2em;font-size:1.2em;margin:6px 0}.home-login .full{width:100%;margin-bottom:0.25em}.home-login #id_password{width:60%}.home-login .button{font-size:1em;width:30%;max-width:30%;float:right}.home-login h3{font-weight:bold;padding:0 0 0.5em 0}.home-login h1{font-size:4rem}input[colorpicker=true]{width:6em !important}.login-background,.home-background{background-image:url("../img/background.jpg");background-color:1}.login-background h1,.login-background label,.home-background h1,.home-background label{text-shadow:0px 0px 12px rgba(251,255,219,0.7);color:#fbffdb;font-size:2em}.login-background p,.home-background p{color:rgba(255,255,255,0.5)}.login-background a,.home-background a{color:black;text-shadow:1px 1px 1px rgba(255,255,255,0.3)}.login-background .alert,.home-background .alert{float:left;width:500px}.login-background .alert span,.home-background .alert span{margin-top:1em;width:1em;display:inline-block;font-size:3em;color:rgba(0,0,0,0.3);text-shadow:1px 1px 1px rgba(255,255,255,0.1)}.invitation{margin:0.5em;box-sizing:border-box;border:1px solid rgba(0,0,0,0.1)}.invitation p{padding:0.5em}.invitation .invit-actions{background-color:rgba(0,0,0,0.1);padding:0.5em}.invitation .invit-actions form{display:inline-block}textarea[name=as_a]{height:3rem}textarea[name=so_i_can]{height:6rem}textarea[name=i_want_to]{height:6rem}.percent-cell{font-size:1rem;border:1px solid rgba(0,0,0,0.3);box-sizing:border-box}.percent-cell .name{display:block;width:100%;clear:both;padding:0.25em}.percent-cell .percent{font-size:0.75em;color:rgba(0,0,0,0.5);text-align:left;line-height:1.25em;padding-left:0.25em;clear:both;display:block;height:1rem;min-height:1rem;background-color:#eee;border:1px solid rgba(0,0,0,0.2);box-sizing:border-box;overflow-x:visible;white-space:nowrap}.legend{border:1px solid rgba(0,0,0,0.2);font-size:0.75rem;text-align:left;line-height:1.25em;padding:0.25em}.bg-green{background-color:#c8ffe6 !important}.bg-blue{background-color:#c8e6ff !important}.search input{font-size:1rem}.project-head .name{display:inline-block;font-size:1.5em;margin:0.5em 0}.project-head a{font-size:0.75em;vertical-align:text-top}/* * Font Awesome 3.1.0 * the iconic font designed for Bootstrap * ------------------------------------------------------- diff --git a/facile_backlog/core/templates/profile/api_key_form.html b/facile_backlog/core/templates/profile/api_key_form.html new file mode 100644 index 0000000..6a31768 --- /dev/null +++ b/facile_backlog/core/templates/profile/api_key_form.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}{% trans "Change API key" %}{% endblock %} + +{% block content %} +

{% trans "Change API key" %}

+

+ {% if has_token %} + {% blocktrans %}You're about to request a new API key, your previous key will be revoked. Are you sure your want to do that? {% endblocktrans %} + {% else %} + {% blocktrans %}You're about to create an API key for your account.{% endblocktrans %} + {% endif %} +

+
+ {% include "form.html" %} + +
+{% endblock %} diff --git a/facile_backlog/core/templates/profile/user_form.html b/facile_backlog/core/templates/profile/user_form.html index ac1c68c..482140e 100644 --- a/facile_backlog/core/templates/profile/user_form.html +++ b/facile_backlog/core/templates/profile/user_form.html @@ -6,13 +6,32 @@

{% trans "Update your profile" %}

+ + +
+ + {% if request.user.auth_token %} +
{{ request.user.auth_token.key }}
+ {% trans "Change API key" %} +
+ {% else %} + + {% trans "You don't have any API key yet" %} + {% trans "Create one" %} + + {% endif %} +
+
- - {{ user.email }} + + {{ user.email }}
{% include "form.html" %}
- +
+ +

{% trans "API" %}

+ {% trans "API Help" %} {% endblock %} diff --git a/facile_backlog/core/urls.py b/facile_backlog/core/urls.py index 13d1c6b..1080681 100644 --- a/facile_backlog/core/urls.py +++ b/facile_backlog/core/urls.py @@ -34,4 +34,7 @@ url(r'^reset/(?P[\w:-]+)/$', views.reset, name='password_reset_reset'), + + url(r'^change_api_key/$', views.change_api_key, + name='change_api_key'), ) diff --git a/facile_backlog/core/views.py b/facile_backlog/core/views.py index 7103320..e175169 100644 --- a/facile_backlog/core/views.py +++ b/facile_backlog/core/views.py @@ -18,7 +18,7 @@ from ratelimitbackend.views import login as do_login from .forms import (ProfileEditionForm, RegistrationForm, PasswordRecoveryForm, - PasswordResetForm) + PasswordResetForm, ChangeApiKeyForm) def login(request): @@ -168,3 +168,30 @@ def form_valid(self, form): _('Your password was successfully reset.')) return redirect(self.get_success_url()) reset = Reset.as_view() + + +class ChangeAPIKey(generic.FormView): + form_class = ChangeApiKeyForm + template_name = 'profile/api_key_form.html' + success_url = reverse_lazy('auth_profile') + + def get_form_kwargs(self): + kwargs = super(ChangeAPIKey, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def get_context_data(self, **kwargs): + data = super(ChangeAPIKey, self).get_context_data(**kwargs) + data['has_token'] = hasattr(self.request.user, "auth_token") + return data + + def form_valid(self, form): + created = form.change_or_create() + if created: + messages.success(self.request, + _('API key successfully created.')) + else: + messages.success(self.request, + _('API key successfully changed.')) + return redirect(self.get_success_url()) +change_api_key = login_required(ChangeAPIKey.as_view()) diff --git a/facile_backlog/settings.py b/facile_backlog/settings.py index 5b3da81..688919f 100644 --- a/facile_backlog/settings.py +++ b/facile_backlog/settings.py @@ -158,6 +158,8 @@ 'facile_backlog', 'facile_backlog.backlog', 'south', + 'rest_framework', + 'rest_framework.authtoken' # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', ) @@ -170,6 +172,18 @@ 'dsn': os.environ['SENTRY_DSN'], } + +#Rest framework +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated' + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ) +} + # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. @@ -200,7 +214,7 @@ 'sentry': { 'level': 'ERROR', 'class': - 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'raven.contrib.django.raven_compat.handlers.SentryHandler', }, }, 'loggers': { diff --git a/facile_backlog/urls.py b/facile_backlog/urls.py index ac34541..5b63fa4 100644 --- a/facile_backlog/urls.py +++ b/facile_backlog/urls.py @@ -28,6 +28,8 @@ url(r'^', include('facile_backlog.backlog.urls')), + url(r'^api/', include('facile_backlog.api.urls')), + url(r'^admin/', include(admin.site.urls)), url(r'^404$', page_404), diff --git a/facile_backlog/views.py b/facile_backlog/views.py index 7b912d2..5743fb2 100644 --- a/facile_backlog/views.py +++ b/facile_backlog/views.py @@ -1,4 +1,3 @@ -from django.http.response import Http404 from django.core.urlresolvers import reverse from django.shortcuts import redirect, render from django.views.generic import TemplateView @@ -38,4 +37,4 @@ def page_404(request): def page_500(request): response = render(request, "500.html") response.status_code = 500 - return response \ No newline at end of file + return response diff --git a/requirements.txt b/requirements.txt index dd91444..02b3daa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,12 @@ bleach==1.2.1 dj-database-url==0.2.1 Django==1.5.1 django-extensions==1.1.1 +django-filter==0.6 django-le-social==0.8 django-password-reset==0.4 django-ratelimit-backend==0.6 django-sekizai==0.7 +djangorestframework==2.3.5 html5lib==0.95 Markdown==2.2.1 palette==0.2 diff --git a/scss/partials/_global.scss b/scss/partials/_global.scss index 3e199cc..1860b08 100644 --- a/scss/partials/_global.scss +++ b/scss/partials/_global.scss @@ -243,6 +243,19 @@ form { margin: 0 0 10px 214px; } } + .read-only-field { + vertical-align: sub; + color: $gray-text-color; + } + pre { + font-size: 1.2em; + font-family: monospace; + display: inline-block; + color: $text-color; + background-color: white; + border: 1px dashed rgba(0,0,0,0.2); + padding: 2px; + } .submit { padding-left: 220px; input { diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dd26f24 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +exclude = migrations diff --git a/tests/__init__.py b/tests/__init__.py index 57b1dee..f468486 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,6 +2,9 @@ import random import re import string +import json + +from django.test import TestCase, Client TEST_DATA = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data') @@ -63,3 +66,30 @@ def line_starting(text, start): if l.find(start) == 0: return l return None + + +class ApiClient(Client): + def request(self, **request): + response = super(ApiClient, self).request(**request) + if response.get("Content-Type", "").find('application/json') != -1: + response.json = json.loads(response.content) + return response + + +class JsonTestCase(TestCase): + client_class = ApiClient + + def assertJsonKeyEqual(self, response, key, reference, status_code=200): # noqa + self.assertEqual(response.status_code, status_code) + json1 = response.json + if key not in json1: + raise AssertionError("Json content has no key '{0}'".format(key)) + if json1[key] != reference: + raise AssertionError( + "json content ['{0}]' not equal" + " '{1}' != '{2}'".format(key, json1[key], reference) + ) + + +def api_token_auth(token): + return {'HTTP_AUTHORIZATION': 'Token {0}'.format(token)} \ No newline at end of file diff --git a/tests/factories.py b/tests/factories.py index 9d45eed..3321702 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -4,10 +4,10 @@ from facile_backlog.backlog.models import (Project, UserStory, Backlog, AuthorizationAssociation) +from facile_backlog.core.models import User from . import rand_lorem_phrase, rand_email -from facile_backlog.core.models import User class ProjectFactory(Factory): diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..832e578 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,123 @@ +import json + +from django.core.urlresolvers import reverse +from rest_framework.authtoken.models import Token + +from .factories import (UserFactory, create_sample_project, + create_sample_backlog, create_sample_story) + + +from . import JsonTestCase, api_token_auth + + +def user_token_auth(user): + token, create = Token.objects.get_or_create(user=user) + return api_token_auth(token) + + +class APITest(JsonTestCase): + + def test_api_home(self): + url = reverse("api_home") + self.client.get(url, status=401) + + user = UserFactory.create(email="test@test.ch") + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertContains(response, "projects//") + + def test_api_project_list(self): + user = UserFactory.create(email="test@test.ch") + project = create_sample_project(user, project_kwargs={ + 'name': "My project", + 'description': "My description" + }) + + url = reverse("api_project_list") + self.client.get(url, status=401) + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertEqual(len(response.json), 1) + self.assertEqual(response.json[0]['name'], "My project") + self.assertEqual(response.json[0]['description'], "My description") + self.assertEqual(response.json[0]['id'], project.pk) + self.assertEqual(response.json[0]['url'], + "http://testserver/api/projects/1/") + + def test_api_project_detail(self): + user = UserFactory.create(email="test@test.ch") + project = create_sample_project(user, project_kwargs={ + 'name': "My project", + }) + url = reverse("api_project_detail", args=(project.pk,)) + self.client.get(url, status=401) + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertJsonKeyEqual(response, 'name', "My project") + self.assertJsonKeyEqual(response, 'id', project.pk) + + def test_api_backlog_list(self): + user = UserFactory.create(email="test@test.ch") + backlog = create_sample_backlog(user, backlog_kwargs={ + 'name': "My backlog", + 'description': "My description" + }) + url = reverse("api_backlog_list", args=(backlog.project.pk,)) + self.client.get(url, status=401) + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertEqual(len(response.json), 1) + self.assertEqual(response.json[0]['name'], "My backlog") + self.assertEqual(response.json[0]['description'], "My description") + self.assertEqual(response.json[0]['url'], + "http://testserver/api/projects/1/backlogs/1/") + + def test_api_backlog_detail(self): + user = UserFactory.create(email="test@test.ch") + backlog = create_sample_backlog(user, backlog_kwargs={ + 'name': "My backlog", + }) + url = reverse("api_backlog_detail", args=( + backlog.project_id, backlog.pk)) + self.client.get(url, status=401) + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertJsonKeyEqual(response, 'name', "My backlog") + self.assertJsonKeyEqual(response, 'id', backlog.pk) + + def test_api_story_list(self): + user = UserFactory.create(email="test@test.ch") + story = create_sample_story(user, story_kwargs={ + 'as_a': "Test writer", + 'i_want_to': "be able to run test", + 'so_i_can': "know if my tests pass" + }) + url = reverse("api_story_list", args=(story.project.pk, + story.backlog.pk)) + self.client.get(url, status=401) + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertEqual(len(response.json), 1) + self.assertEqual(response.json[0]['as_a'], "Test writer") + self.assertEqual(response.json[0]['i_want_to'], "be able to run test") + self.assertEqual(response.json[0]['so_i_can'], "know if my tests pass") + self.assertEqual(response.json[0]['code'], story.code) + self.assertEqual( + response.json[0]['url'], + "http://testserver/api/projects/1/backlogs/1/stories/1/" + ) + + def test_api_story_detail(self): + user = UserFactory.create(email="test@test.ch") + story = create_sample_story(user, story_kwargs={ + 'as_a': "Test writer", + 'i_want_to': "be able to run test", + 'so_i_can': "know if my tests pass" + }) + url = reverse("api_story_detail", args=( + story.project_id, story.backlog_id, story.pk)) + self.client.get(url, status=401) + auth = user_token_auth(user) + response = self.client.get(url, **auth) + self.assertJsonKeyEqual(response, 'as_a', "Test writer") + self.assertJsonKeyEqual(response, 'id', story.pk) \ No newline at end of file diff --git a/update_prod.sh b/update_prod.sh index 7d1f8b4..e45248d 100755 --- a/update_prod.sh +++ b/update_prod.sh @@ -1,6 +1,7 @@ supervisorctl stop backlogman git pull source env/bin/activate +pip install -r requirements.txt envdir /etc/backlogman.d/ python manage.py migrate envdir /etc/backlogman.d/ python manage.py collectstatic --noinput chmod -R 755 /home/backlogman/public