From bb675ea84d400a26f2cd38a8eeed7270f9cbb13a Mon Sep 17 00:00:00 2001 From: Kasper Brandt Date: Fri, 28 Nov 2014 17:19:05 +0100 Subject: [PATCH 1/5] [#933] First version of 'Add an update' page - Added a page for adding updates - Only accessible for those allowed to add an update - If on a project (update) page, user is directed to the update page directly - Otherwise, the user goes to the 'My Projects' page in MyRSR - Added an 'Add an update' link to every project in 'My Projects', if the user is allowed to post updates for that project To do: - Add location of update - Cleanup code --- akvo/rest/serializers/__init__.py | 3 +- akvo/rest/serializers/project_update.py | 9 ++ akvo/rest/urls.py | 1 + akvo/rest/views/__init__.py | 3 +- akvo/rest/views/project_update.py | 39 +++++++- akvo/rsr/forms.py | 81 ++++++++++++++++- akvo/rsr/views/project.py | 62 ++++++++++++- akvo/rsr/views/project_update.py | 6 +- akvo/templates/myrsr/my_projects.html | 13 ++- akvo/templates/navigation/header.html | 7 +- akvo/templates/update_add.html | 115 ++++++++++++++++++++++++ akvo/urls/rsr.py | 4 + 12 files changed, 329 insertions(+), 14 deletions(-) create mode 100644 akvo/templates/update_add.html diff --git a/akvo/rest/serializers/__init__.py b/akvo/rest/serializers/__init__.py index 3213e46acf..6e73c84c74 100644 --- a/akvo/rest/serializers/__init__.py +++ b/akvo/rest/serializers/__init__.py @@ -24,7 +24,7 @@ from .project import ProjectSerializer from .project_comment import ProjectCommentSerializer from .project_location import ProjectLocationSerializer -from .project_update import ProjectUpdateSerializer, ProjectUpdateExtraSerializer +from .project_update import BaseProjectUpdateSerializer, ProjectUpdateSerializer, ProjectUpdateExtraSerializer from .project_update_location import ProjectUpdateLocationSerializer from .publishing_status import PublishingStatusSerializer from .user import UserSerializer, UserDetailsSerializer, UserPasswordSerializer @@ -50,6 +50,7 @@ 'ProjectSerializer', 'ProjectCommentSerializer', 'ProjectLocationSerializer', + 'BaseProjectUpdateSerializer', 'ProjectUpdateSerializer', 'ProjectUpdateExtraSerializer', 'ProjectUpdateLocationSerializer', diff --git a/akvo/rest/serializers/project_update.py b/akvo/rest/serializers/project_update.py index 0134700a83..1559be9d66 100644 --- a/akvo/rest/serializers/project_update.py +++ b/akvo/rest/serializers/project_update.py @@ -16,6 +16,15 @@ from .user import UserSerializer +class BaseProjectUpdateSerializer(BaseRSRSerializer): + """Used to post an update without location.""" + + #photo = Base64ImageField(required=False, allow_empty_file=True) + + class Meta: + model = ProjectUpdate + + class ProjectUpdateSerializer(BaseRSRSerializer): locations = ProjectUpdateLocationSerializer(source='locations', many=True) diff --git a/akvo/rest/urls.py b/akvo/rest/urls.py index f5c70e0037..761ca4afa4 100644 --- a/akvo/rest/urls.py +++ b/akvo/rest/urls.py @@ -44,6 +44,7 @@ url(r'^', include(router.urls)), url(r'^employment/(?P[0-9]+)/approve/$', views.approve_employment, name='approve_employment'), url(r'^employment/(?P[0-9]+)/set_group/(?P[0-9]+)/$', views.set_group, name='set_group'), + url(r'^project/(?P[0-9]+)/add_update/$', views.add_update, name='add_update'), url(r'^user/(?P[0-9]+)/change_password/$', views.change_password, name='user_change_password'), url(r'^user/(?P[0-9]+)/update_details/$', views.update_details, name='user_update_details'), url(r'^user/(?P[0-9]+)/request_organisation/$', views.request_organisation, name='user_request_organisation'), diff --git a/akvo/rest/views/__init__.py b/akvo/rest/views/__init__.py index ce3c78bb96..f133b487cb 100644 --- a/akvo/rest/views/__init__.py +++ b/akvo/rest/views/__init__.py @@ -25,7 +25,7 @@ from .project import ProjectViewSet from .project_comment import ProjectCommentViewSet from .project_location import ProjectLocationViewSet -from .project_update import ProjectUpdateViewSet, ProjectUpdateExtraViewSet +from .project_update import ProjectUpdateViewSet, ProjectUpdateExtraViewSet, add_update from .project_update_location import ProjectUpdateLocationViewSet from .publishing_status import PublishingStatusViewSet from .user import UserViewSet, change_password, update_details, request_organisation @@ -56,6 +56,7 @@ 'ProjectUpdateLocationViewSet', 'ProjectUpdateViewSet', 'ProjectUpdateExtraViewSet', + 'add_update', 'PublishingStatusViewSet', 'UserViewSet', 'change_password', diff --git a/akvo/rest/views/project_update.py b/akvo/rest/views/project_update.py index d07f5e7bae..a16b25d451 100644 --- a/akvo/rest/views/project_update.py +++ b/akvo/rest/views/project_update.py @@ -5,9 +5,20 @@ # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >. -from akvo.rsr.models import ProjectUpdate +import cStringIO -from ..serializers import ProjectUpdateSerializer, ProjectUpdateExtraSerializer +from django.shortcuts import get_object_or_404 + +from rest_framework import status +from rest_framework.decorators import api_view, parser_classes, permission_classes +from rest_framework.exceptions import PermissionDenied +from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from akvo.rsr.models import Project, ProjectUpdate + +from ..serializers import BaseProjectUpdateSerializer, ProjectUpdateSerializer, ProjectUpdateExtraSerializer from ..viewsets import BaseRSRViewSet @@ -43,4 +54,26 @@ def get_queryset(self): uuid = self.request.QUERY_PARAMS.get('uuid', None) if uuid is not None: queryset = self.queryset.filter(uuid=uuid) - return queryset \ No newline at end of file + return queryset + + +@api_view(['POST']) +@parser_classes((MultiPartParser, FormParser,)) +@permission_classes((IsAuthenticated, )) +def add_update(request, pk=None): + project = get_object_or_404(Project, pk=pk) + + # Check if user is allowed to post updates to this project + if not request.user.has_perm('rsr.post_updates', project): + raise PermissionDenied() + + request.DATA['project'] = pk + request.DATA['user'] = request.user.pk + + serializer = BaseProjectUpdateSerializer(data=request.DATA, files=request.FILES) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/akvo/rsr/forms.py b/akvo/rsr/forms.py index afe2b75184..2c96044afd 100644 --- a/akvo/rsr/forms.py +++ b/akvo/rsr/forms.py @@ -16,7 +16,11 @@ from registration.models import RegistrationProfile -from .models import Country, Organisation +from urlparse import urlsplit, urlunsplit + +from .models import Country, Organisation, ProjectUpdate + +from akvo import settings class RegisterForm(forms.Form): email = forms.EmailField( @@ -204,3 +208,78 @@ def save(self, request): """ # TODO: The approval process of users request.user.organisations.add(self.cleaned_data['organisation']) + + +class ProjectUpdateForm(forms.ModelForm): + """Form representing a ProjectUpdate.""" + + title = forms.CharField(label='', widget=forms.TextInput(attrs={ + 'class': 'input', + 'size': '42', + 'maxlength': '50', + 'placeholder': 'Title', + })) + text = forms.CharField(label='', required=False, widget=forms.Textarea(attrs={ + 'class': 'textarea', + 'placeholder': 'Description', + })) + language = forms.ChoiceField(choices=settings.LANGUAGES, initial='en') + photo = forms.ImageField(required=False, widget=forms.FileInput(attrs={ + 'class': 'input', + 'size': '15', + })) + photo_caption = forms.CharField(label='', required=False, widget=forms.TextInput(attrs={ + 'class': 'input', + 'size': '25', + 'maxlength': '75', + 'placeholder': 'Photo caption', + })) + photo_credit = forms.CharField(label='', required=False, widget=forms.TextInput(attrs={ + 'class': 'input', + 'size': '25', + 'maxlength': '25', + 'placeholder': 'Photo credit', + })) + video = forms.CharField(required=False, widget=forms.TextInput(attrs={ + 'class': 'input', + 'size': '42', + 'maxlength': '255', + 'placeholder': 'Video link', + })) + video_caption = forms.CharField(label='', required=False, widget=forms.TextInput(attrs={ + 'class': 'input', + 'size': '25', + 'maxlength': '75', + 'placeholder': 'Video caption', + })) + video_credit = forms.CharField(label='', required=False, widget=forms.TextInput(attrs={ + 'class': 'input', + 'size': '25', + 'maxlength': '25', + 'placeholder': 'Video credit', + })) + latitude = forms.FloatField(widget=forms.HiddenInput()) + longitude = forms.FloatField(widget=forms.HiddenInput()) + + class Meta: + model = ProjectUpdate + fields = ('title', 'text', 'language', 'photo', 'photo_caption', 'photo_credit', 'video', 'video_caption', + 'video_credit') + + def clean_video(self): + data = self.cleaned_data['video'] + if data: + scheme, netloc, path, query, fragment = urlsplit(data) + netloc = netloc.lower() + valid_url = (netloc == 'blip.tv' or + netloc == 'vimeo.com' or + netloc == 'www.youtube.com' and path == '/watch' or + netloc == 'youtu.be') + if not valid_url: + raise forms.ValidationError(_('Invalid video URL. Currently ' + 'Blip.TV, Vimeo and YouTube are supported.')) + if netloc == 'youtu.be': + netloc = 'www.youtube.com' + path = '/watch?v=%s' % path.lstrip('/') + data = urlunsplit((scheme, netloc, path, query, fragment)) + return data diff --git a/akvo/rsr/views/project.py b/akvo/rsr/views/project.py index 9c3adcc6b4..5090c21587 100644 --- a/akvo/rsr/views/project.py +++ b/akvo/rsr/views/project.py @@ -8,10 +8,12 @@ import json +from ..forms import ProjectUpdateForm from ..models import Invoice, Project from ...utils import pagination -from django.shortcuts import get_object_or_404, render +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, redirect, render def _get_accordion_data(project): @@ -120,6 +122,64 @@ def main(request, project_id): return render(request, 'project_main.html', context) +@login_required +def add_update_old(request, project_id): + project = get_object_or_404(Project, pk=project_id) + updates = project.updates_desc()[:5] + updateform = ProjectUpdateForm() + + context = { + 'project': project, + 'updates': updates, + 'updateform': updateform, + } + + return render(request, 'update_add.html', context) + +@login_required() +def add_update(request, project_id, edit_mode=False, form_class=ProjectUpdateForm, update_id=None): + project = get_object_or_404(Project, id=project_id) + updates = project.updates_desc()[:5] + update = None + + # if update_id is not None: + # edit_mode = True + # update = get_object_or_404(ProjectUpdate, id=update_id) + # if not request.user == update.user: + # request.error_message = u'You can only edit your own updates.' + # raise PermissionDenied + # + # if update.edit_window_has_expired(): + # return render_to_response('rsr/project/update_form_timeout.html', + # dict( + # project=project, + # update=update, + # site_section='projects', + # ), + # RequestContext(request)) + + if request.method == 'POST': + updateform = form_class(request.POST, request.FILES, instance=update) + if updateform.is_valid(): + update = updateform.save(commit=False) + update.project = project + update.user = request.user + update.update_method = 'W' + update.save() + return redirect(update.get_absolute_url()) + else: + updateform = form_class(instance=update) + + context = { + 'project': project, + 'updates': updates, + 'update': update, + 'updateform': updateform, + } + + return render(request, 'update_add.html', context) + + def search(request): context = {'projects': Project.objects.published()} return render(request, 'project_search.html', context) diff --git a/akvo/rsr/views/project_update.py b/akvo/rsr/views/project_update.py index fb74fb25f6..340c863561 100644 --- a/akvo/rsr/views/project_update.py +++ b/akvo/rsr/views/project_update.py @@ -27,8 +27,10 @@ def directory(request): def main(request, project_id, update_id): - context = {'update': get_object_or_404(ProjectUpdate, pk=update_id, - project=project_id)} + context = { + 'update': get_object_or_404(ProjectUpdate, pk=update_id, project=project_id), + 'project': get_object_or_404(Project, pk=project_id) + } return render(request, 'update_main.html', context) diff --git a/akvo/templates/myrsr/my_projects.html b/akvo/templates/myrsr/my_projects.html index 3de83c0a9d..a665443c53 100644 --- a/akvo/templates/myrsr/my_projects.html +++ b/akvo/templates/myrsr/my_projects.html @@ -7,6 +7,7 @@ {% block myrsr_main %}

{% trans "My project" %}s

+ {% if page %} @@ -44,6 +45,9 @@

{{ project.title }}

{% endif %} {% endif %} + {% has_perm 'rsr.post_updates' project as can_add_update %} +
+ Add an update {% endfor %} @@ -52,7 +56,12 @@

{{ project.title }}

Viewing {{ page.start_index }} - {{ page.end_index }} of {{ paginator.count }} projects

{% include 'navigation/pagination.html' %} - + {% else %} +

You can't see any projects yet.

+

+ Go to the My details page to request to join an organisation. + Once your request is approved, you will see the projects of this organisation on this page. +

+ {% endif %} - {% endblock %} diff --git a/akvo/templates/navigation/header.html b/akvo/templates/navigation/header.html index 91f7af9891..044f37949e 100644 --- a/akvo/templates/navigation/header.html +++ b/akvo/templates/navigation/header.html @@ -2,6 +2,7 @@ {% url 'project-directory' as project_url %} {% url 'update-directory' as update_url %} +{% url 'update-directory' as update_url %} {% url 'organisation-directory' as organisation_url %} {% url 'my_details' as myrsr_url %} @@ -25,8 +26,7 @@