Project for the UPV to develop an app like BlaBlaCar but only for UPV people.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

first version for messages

+1062 -59
+2
package.json
··· 7 7 "dependencies": {}, 8 8 "devDependencies": { 9 9 "angular": "^1.5.5", 10 + "babel-polyfill": "^6.9.1", 10 11 "babel-preset-es2015": "^6.6.0", 11 12 "babel-preset-react": "^6.5.0", 12 13 "babelify": "^7.3.0", ··· 21 22 "gulp-uglify": "^1.5.3", 22 23 "gulp-util": "^3.0.7", 23 24 "jquery": "^2.1.4", 25 + "moment": "^2.13.0", 24 26 "node-sass": "^3.4.2", 25 27 "tether": "^1.3.2", 26 28 "vinyl-buffer": "^1.0.0",
+1 -1
requirements/base.txt
··· 1 - django==1.9.6 1 + django==1.9.7 2 2 django-environ==0.4.0 3 3 django-braces==1.8.1 4 4 django-floppyforms==1.6.2
+3 -1
upvcarshare/config/router.py
··· 5 5 from rest_framework.routers import SimpleRouter 6 6 7 7 from journeys.api.v1.resources import TransportResource, ResidenceResource, CampusResource, JourneyResource, \ 8 - join_journey, leave_journey, recommended_journeys, cancel_journey 8 + join_journey, leave_journey, recommended_journeys, cancel_journey, journey_messages, MessageResource 9 9 from notifications.api.v1.resources import NotificationResource 10 10 from users.api.v1.resources import me 11 11 ··· 14 14 router.register(r'transports', viewset=TransportResource) 15 15 router.register(r'residences', viewset=ResidenceResource) 16 16 router.register(r'campus', viewset=CampusResource) 17 + router.register(r'messages', viewset=MessageResource) 17 18 router.register(r'journeys', viewset=JourneyResource) 18 19 router.register(r'notifications', viewset=NotificationResource) 19 20 ··· 23 24 url(r'^journeys/(?P<pk>[^/.]+)/recommended/$', recommended_journeys, name='recommended-journeys'), 24 25 url(r'^journeys/(?P<pk>[^/.]+)/join/$', join_journey, name='join-journey'), 25 26 url(r'^journeys/(?P<pk>[^/.]+)/leave/$', leave_journey, name='leave-journey'), 27 + url(r'^journeys/(?P<pk>[^/.]+)/messages/$', journey_messages, name='journey-messages'), 26 28 url(r'^users/me/$', me, kwargs={'pk': 'me'}), 27 29 url(r'^', include(router.urls)), 28 30 ]
+10 -4
upvcarshare/config/urls.py
··· 2 2 from django.conf import settings 3 3 from django.conf.urls import url, include 4 4 from django.contrib import admin 5 - from django.views import defaults as default_views 6 - from django.views.generic import TemplateView 7 5 from django.utils.translation import ugettext_lazy as _ 6 + from django.views import defaults as default_views 8 7 9 8 from config.router import urlpatterns as api_urlpatterns 9 + # App URLs 10 + from core.views import PartialsTemplateView 11 + from pages.views import HomeView 10 12 11 - # App URLs 12 13 urlpatterns = [ 13 - url(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"), 14 + url(r"^$", HomeView.as_view(), name="home"), 14 15 url(r"^", include("pages.urls", namespace="pages")), 15 16 url(r"^users/", include("users.urls", namespace="users")), 16 17 url(r"^journeys/", include("journeys.urls", namespace="journeys")), 17 18 url(r"^notifications/", include("notifications.urls", namespace="notifications")), 19 + ] 20 + 21 + # Partials URLs 22 + urlpatterns += [ 23 + url(r'^partials/(?P<name>.+)\.html', PartialsTemplateView.as_view(), name="partials-template"), 18 24 ] 19 25 20 26 # Admin URLs
+7
upvcarshare/core/templatetags/core_tags.py
··· 22 22 query_dict = QueryDict(mutable=True) 23 23 query_dict.update(args) 24 24 return "{}?{}".format(base_uri, query_dict.urlencode()) 25 + 26 + 27 + @register.simple_tag(takes_context=True) 28 + def add_active_class(context, names, _class="active"): 29 + request = context["request"] 30 + names = names.split(",") 31 + return _class if request.resolver_match.view_name in names else ""
+30
upvcarshare/core/views.py
··· 1 + # -*- coding: utf-8 -*- 2 + from __future__ import unicode_literals, print_function, absolute_import 3 + 4 + from django.http import Http404 5 + from django.shortcuts import render 6 + from django.template import TemplateDoesNotExist 7 + from django.views.generic import View 8 + 9 + 10 + class PartialsTemplateView(View): 11 + """Generic view for load templates from frontend app. Takes an argument form 12 + url definition, and use it to resolve the requested template. 13 + """ 14 + external_templates_folder = 'partials/' 15 + 16 + def get(self, request, name): 17 + """Needs a ``name`` attribute to find the external template. 18 + 19 + :param request: 20 + :type request: HttpRequest 21 + :param name: 22 + :type name: str 23 + :return: HttpResponse 24 + """ 25 + template = "{}{}.html".format(self.external_templates_folder, name) 26 + try: 27 + return render(request, template) 28 + except TemplateDoesNotExist: 29 + raise Http404 30 +
+7
upvcarshare/journeys/__init__.py
··· 8 8 (RETURN, _("vuelta")), 9 9 ) 10 10 11 + CONFIRMED, REJECTED, UNKNOWN = 0, 1, 2 12 + PASSENGER_STATUSES = ( 13 + (CONFIRMED, _("Confiramado")), 14 + (REJECTED, _("Rechazado")), 15 + (UNKNOWN, _("Desconocido")) 16 + ) 17 + 11 18 # Uses projected coordinate system for Spain. 12 19 # See: https://epsg.io/2062 13 20 DEFAULT_PROJECTED_SRID = 2062
+69 -3
upvcarshare/journeys/api/v1/resources.py
··· 1 1 # -*- coding: utf-8 -*- 2 2 from __future__ import unicode_literals, print_function, absolute_import 3 3 4 + from django.http import Http404 4 5 from rest_framework import viewsets, status 6 + from rest_framework.exceptions import NotFound, PermissionDenied 5 7 from rest_framework.generics import get_object_or_404 6 8 from rest_framework.permissions import IsAuthenticated 7 9 from rest_framework.response import Response 8 10 9 - from journeys.api.v1.serializers import TransportSerializer, ResidenceSerializer, CampusSerializer, JourneySerializer 10 - from journeys.models import Transport, Residence, Campus, Journey 11 + from journeys.api.v1.serializers import TransportSerializer, ResidenceSerializer, CampusSerializer, JourneySerializer, \ 12 + MessageSerializer 13 + from journeys.exceptions import UserNotAllowed 14 + from journeys.models import Transport, Residence, Campus, Journey, Message 11 15 12 16 13 17 class TransportResource(viewsets.ModelViewSet): ··· 38 42 IsAuthenticated, 39 43 ] 40 44 45 + def get_queryset(self): 46 + """Only gets residences from user.""" 47 + queryset = super(ResidenceResource, self).get_queryset() 48 + queryset = queryset.filter(user=self.request.user) 49 + return queryset 50 + 41 51 def perform_create(self, serializer): 42 52 """On create, set the user who makes the request the owner.s""" 43 53 serializer.save(user=self.request.user) ··· 106 116 107 117 class RecommendedJourneysResource(viewsets.ReadOnlyModelViewSet): 108 118 """Get recommended journeys for a given journey or for all journeys of the user. 109 - Eg: 119 + 120 + Example: 110 121 111 122 GET /api/v1/journeys/recommended/ 112 123 GET /api/v1/journeys/(id)/recommended/ ··· 159 170 160 171 cancel_journey = CancelJourneyResource.as_view({"post": "cancel"}) 161 172 173 + 174 + class JourneyMessageResource(viewsets.ModelViewSet): 175 + """List the messages of a journey. 176 + 177 + Example: 178 + GET /api/v1/journeys/(id)/messages/ 179 + """ 180 + 181 + queryset = Message.objects.order_by("created") 182 + serializer_class = MessageSerializer 183 + permission_classes = [ 184 + IsAuthenticated, 185 + ] 186 + 187 + def messages(self, request, **kwargs): 188 + pk = kwargs.get('pk', None) 189 + if pk is not None: 190 + journey = get_object_or_404(Journey, pk=pk) 191 + else: 192 + raise NotFound() 193 + try: 194 + queryset = Message.objects.list(user=request.user, journey=journey).order_by("created") 195 + except UserNotAllowed: 196 + raise PermissionDenied() 197 + page = self.paginate_queryset(queryset) 198 + if page is not None: 199 + serializer = self.get_serializer(page, many=True) 200 + return self.get_paginated_response(serializer.data) 201 + serializer = self.get_serializer(queryset, many=True) 202 + return Response(serializer.data) 203 + 204 + journey_messages = JourneyMessageResource.as_view({"get": "messages"}) 205 + 206 + 207 + class MessageResource(viewsets.ModelViewSet): 208 + """Resource for messages.""" 209 + 210 + queryset = Message.objects.order_by("created") 211 + serializer_class = MessageSerializer 212 + permission_classes = [ 213 + IsAuthenticated, 214 + ] 215 + 216 + def get_queryset(self): 217 + journey = self.request.query_params.get("journey") 218 + if journey: 219 + try: 220 + journey = Journey.objects.get(pk=journey) 221 + return Message.objects.list(user=self.request.user, journey=journey).order_by("created") 222 + except Journey.DoesNotExist: 223 + pass 224 + return Message.objects.list(user=self.request.user).order_by("created") 225 + 226 + def perform_create(self, serializer): 227 + serializer.save(user=self.request.user)
+20 -3
upvcarshare/journeys/api/v1/serializers.py
··· 6 6 7 7 from journeys import DEFAULT_WGS84_SRID 8 8 from journeys.helpers import make_point_projected 9 - from journeys.models import Transport, Place, Residence, Campus, Journey 9 + from journeys.models import Transport, Place, Residence, Campus, Journey, Message 10 + from notifications import MESSAGE 11 + from notifications.decorators import dispatch 12 + from notifications.helpers import dispatch_message 10 13 from users.api.v1.serializers import UserSerializer 11 14 12 15 ··· 23 26 24 27 class Meta: 25 28 model = Place 26 - fields = ["name", "distance", "position"] 29 + fields = ["id", "name", "distance", "position"] 27 30 28 31 def validate(self, attrs): 29 32 if 'get_position_wgs84' in attrs: ··· 37 40 38 41 class Meta(PlaceSerializer.Meta): 39 42 model = Residence 40 - fields = ["name", "distance", "position", "address"] 43 + fields = ["id", "name", "distance", "position", "address"] 41 44 42 45 43 46 class CampusSerializer(PlaceSerializer): ··· 59 62 model = Journey 60 63 fields = ["user", "driver", "residence", "campus", "kind", "free_places", "departure", "disabled", 61 64 "current_free_places"] 65 + 66 + 67 + class MessageSerializer(serializers.ModelSerializer): 68 + 69 + user = UserSerializer(read_only=True) 70 + journey = serializers.PrimaryKeyRelatedField(queryset=Journey.objects.all()) 71 + 72 + class Meta: 73 + model = Message 74 + fields = ["user", "journey", "content", "created"] 75 + 76 + @dispatch(MESSAGE) 77 + def save(self, **kwargs): 78 + return super(MessageSerializer, self).save(**kwargs)
+4
upvcarshare/journeys/exceptions.py
··· 12 12 13 13 class NotAPassenger(Exception): 14 14 msg = "The user is not a passenger of this journey." 15 + 16 + 17 + class UserNotAllowed(Exception): 18 + msg = "The user is not a allowed to do this action."
+83 -3
upvcarshare/journeys/forms.py
··· 3 3 4 4 import floppyforms 5 5 from django import forms 6 + from django.core.exceptions import ObjectDoesNotExist 6 7 from django.utils.translation import ugettext_lazy as _ 7 8 8 9 from core.widgets import GMapsPointWidget 9 - from journeys import JOURNEY_KINDS 10 + from journeys import JOURNEY_KINDS, GOING, RETURN 10 11 from journeys.helpers import make_point_projected 11 - from journeys.models import Residence, Journey 12 + from journeys.models import Residence, Journey, Campus 12 13 from users.models import User 13 14 14 15 ··· 47 48 48 49 class JourneyForm(forms.ModelForm): 49 50 50 - i_am_driver = forms.BooleanField(label=_("¿Soy conductor?"), required=False) 51 + i_am_driver = forms.BooleanField( 52 + label=_("¿Soy conductor?"), 53 + required=False, 54 + initial=False, 55 + widget=forms.RadioSelect( 56 + choices=((True, _('Sí')), (False, _('No'))), 57 + ) 58 + ) 51 59 52 60 class Meta: 53 61 model = Journey ··· 76 84 journey = super(JourneyForm, self).save(commit=False) 77 85 journey.user = user 78 86 journey.driver = user if self.cleaned_data["i_am_driver"] else None 87 + if commit: 88 + journey.save() 89 + return journey 90 + 91 + 92 + class SmartJourneyForm(forms.ModelForm): 93 + 94 + origin = forms.CharField(widget=forms.HiddenInput()) 95 + destiny = forms.CharField(widget=forms.HiddenInput()) 96 + 97 + i_am_driver = forms.BooleanField( 98 + label=_("¿Soy conductor?"), 99 + required=False, 100 + initial=False, 101 + widget=forms.RadioSelect( 102 + choices=((True, _('Sí')), (False, _('No'))), 103 + ) 104 + ) 105 + 106 + class Meta: 107 + model = Journey 108 + fields = ["origin", "destiny", "i_am_driver", "free_places", "departure", "time_window"] 109 + widgets = { 110 + "kind": forms.Select(attrs={"class": "form-control"}), 111 + "free_places": forms.NumberInput(attrs={"class": "form-control"}), 112 + "departure": floppyforms.DateTimeInput(attrs={"class": "form-control"}), 113 + "time_window": forms.NumberInput(attrs={"class": "form-control"}), 114 + } 115 + 116 + def __init__(self, *args, **kwargs): 117 + self.user = kwargs.pop("user") 118 + super(SmartJourneyForm, self).__init__(*args, **kwargs) 119 + 120 + def clean_origin(self): 121 + origin = self.cleaned_data["origin"] 122 + data = origin.split(":") 123 + models = {"residence": Residence, "campus": Campus} 124 + try: 125 + return models.get(data[0]).objects.get(pk=data[1]) 126 + except (ObjectDoesNotExist, IndexError, AttributeError): 127 + raise forms.ValidationError(_("Lugar de origen no válido")) 128 + 129 + def clean_destiny(self): 130 + destiny = self.cleaned_data["destiny"] 131 + data = destiny.split(":") 132 + models = {"residence": Residence, "campus": Campus} 133 + try: 134 + return models.get(data[0]).objects.get(pk=data[1]) 135 + except (ObjectDoesNotExist, IndexError, AttributeError): 136 + raise forms.ValidationError(_("Lugar de destino no válido")) 137 + 138 + def save(self, commit=True, **kwargs): 139 + """When save a journey form, you have to provide an user.""" 140 + user = self.user 141 + if "user" in kwargs: 142 + assert isinstance(kwargs["user"], User) 143 + user = kwargs.get("user") 144 + journey = super(SmartJourneyForm, self).save(commit=False) 145 + journey.user = user 146 + journey.driver = user if self.cleaned_data["i_am_driver"] else None 147 + # Smart origin, destiny and kind 148 + origin = self.cleaned_data["origin"] 149 + destiny = self.cleaned_data["destiny"] 150 + attribute_selector = { 151 + Residence: "residence", 152 + Campus: "campus", 153 + } 154 + attribute = attribute_selector[origin.__class__] 155 + setattr(journey, attribute, origin) 156 + attribute = attribute_selector[destiny.__class__] 157 + setattr(journey, attribute, destiny) 158 + journey.kind = GOING if isinstance(origin, Residence) else RETURN 79 159 if commit: 80 160 journey.save() 81 161 return journey
+25
upvcarshare/journeys/managers.py
··· 10 10 from django.utils import timezone 11 11 12 12 from journeys import GOING, RETURN 13 + from journeys.exceptions import UserNotAllowed 14 + from notifications import MESSAGE 15 + from notifications.decorators import dispatch 13 16 14 17 15 18 def recommended_condition(journey, override_distance=None): ··· 139 142 def passenger(self, user): 140 143 """Gets the journeys where the given user is passenger.""" 141 144 return self.filter(disabled=False, passengers__user=user).order_by("departure") 145 + 146 + 147 + class MessageManager(models.Manager): 148 + """Manager to handle messages. """ 149 + 150 + def list(self, user, journey=None): 151 + """Gets the list of all messages the given user could read. 152 + :param user: 153 + :param journey: 154 + """ 155 + if journey is None: 156 + return self.filter(Q(journey__user=user) | Q(journey__passengers__user=user)) 157 + if not journey.is_messenger_allowed(user): 158 + return self.none() 159 + return self.filter(journey=journey) 160 + 161 + @dispatch(MESSAGE) 162 + def send(self, user, message, journey): 163 + """User tries send 'message' to 'journey' group.""" 164 + if not journey.is_messenger_allowed(user): 165 + raise UserNotAllowed() 166 + return self.create(user=user, journey=journey, content=message)
+52
upvcarshare/journeys/migrations/0005_auto_20160610_0919.py
··· 1 + # -*- coding: utf-8 -*- 2 + # Generated by Django 1.9.5 on 2016-06-10 09:19 3 + from __future__ import unicode_literals 4 + 5 + from django.conf import settings 6 + from django.db import migrations, models 7 + import django.db.models.deletion 8 + import django_extensions.db.fields 9 + 10 + 11 + class Migration(migrations.Migration): 12 + 13 + dependencies = [ 14 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 + ('journeys', '0004_journey_time_window'), 16 + ] 17 + 18 + operations = [ 19 + migrations.CreateModel( 20 + name='Message', 21 + fields=[ 22 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), 24 + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), 25 + ('content', models.TextField()), 26 + ], 27 + options={ 28 + 'get_latest_by': 'modified', 29 + 'abstract': False, 30 + 'ordering': ('-modified', '-created'), 31 + }, 32 + ), 33 + migrations.AlterModelOptions( 34 + name='campus', 35 + options={'verbose_name_plural': 'campuses'}, 36 + ), 37 + migrations.AlterField( 38 + model_name='journey', 39 + name='kind', 40 + field=models.PositiveIntegerField(choices=[(0, 'ida'), (1, 'vuelta')], verbose_name='tipo de trayecto'), 41 + ), 42 + migrations.AddField( 43 + model_name='message', 44 + name='journey', 45 + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='journeys.Journey'), 46 + ), 47 + migrations.AddField( 48 + model_name='message', 49 + name='user', 50 + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to=settings.AUTH_USER_MODEL), 51 + ), 52 + ]
+20
upvcarshare/journeys/migrations/0006_passenger_status.py
··· 1 + # -*- coding: utf-8 -*- 2 + # Generated by Django 1.9.7 on 2016-06-19 17:06 3 + from __future__ import unicode_literals 4 + 5 + from django.db import migrations, models 6 + 7 + 8 + class Migration(migrations.Migration): 9 + 10 + dependencies = [ 11 + ('journeys', '0005_auto_20160610_0919'), 12 + ] 13 + 14 + operations = [ 15 + migrations.AddField( 16 + model_name='passenger', 17 + name='status', 18 + field=models.PositiveIntegerField(choices=[(0, 'Confiramado'), (1, 'Rechazado'), (2, 'Desconocido')], default=2), 19 + ), 20 + ]
+17 -2
upvcarshare/journeys/models.py
··· 15 15 16 16 from core.models import GisTimeStampedModel 17 17 from journeys import JOURNEY_KINDS, GOING, RETURN, DEFAULT_DISTANCE, DEFAULT_PROJECTED_SRID, DEFAULT_WGS84_SRID, \ 18 - DEFAULT_TIME_WINDOW 18 + DEFAULT_TIME_WINDOW, PASSENGER_STATUSES, UNKNOWN 19 19 from journeys.exceptions import NoFreePlaces, NotAPassenger, AlreadyAPassenger 20 20 from journeys.helpers import make_point_wgs84 21 - from journeys.managers import JourneyManager, ResidenceManager 21 + from journeys.managers import JourneyManager, ResidenceManager, MessageManager 22 22 from notifications import JOIN, LEAVE, CANCEL 23 23 from notifications.decorators import dispatch 24 24 ··· 236 236 """Gets the journey distance.""" 237 237 return self.residence.position.distance(self.campus.position) / 1000 238 238 239 + def is_messenger_allowed(self, user): 240 + """Check if the user is allowed to make messenger actions.""" 241 + return not(self.user != user and not self.is_passenger(user)) 242 + 239 243 @dispatch(CANCEL) 240 244 def cancel(self): 241 245 """Cancels a journey.""" ··· 247 251 """A user who has joined a journey.""" 248 252 user = models.ForeignKey(settings.AUTH_USER_MODEL) 249 253 journey = models.ForeignKey("journeys.Journey", related_name="passengers") 254 + status = models.PositiveIntegerField(choices=PASSENGER_STATUSES, default=UNKNOWN) 250 255 251 256 class Meta: 252 257 unique_together = ["user", "journey"] ··· 257 262 name = models.CharField(max_length=64, blank=True, null=True) 258 263 user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="transports") 259 264 default_places = models.PositiveIntegerField(default=4) 265 + 266 + 267 + class Message(TimeStampedModel): 268 + """Message send by a passenger of the journey.""" 269 + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="messages") 270 + journey = models.ForeignKey("journeys.Journey", related_name="messages") 271 + content = models.TextField() 272 + 273 + objects = MessageManager() 274 +
+6
upvcarshare/journeys/tests/factories.py
··· 44 44 45 45 class Meta: 46 46 model = "journeys.Transport" 47 + 48 + 49 + class MessageFactory(factory.django.DjangoModelFactory): 50 + 51 + class Meta: 52 + model = "journeys.Message"
+71 -1
upvcarshare/journeys/tests/test_api.py
··· 10 10 11 11 from journeys import GOING, RETURN, DEFAULT_PROJECTED_SRID 12 12 from journeys.models import Transport, Journey 13 - from journeys.tests.factories import TransportFactory, ResidenceFactory, CampusFactory, JourneyFactory 13 + from journeys.tests.factories import TransportFactory, ResidenceFactory, CampusFactory, JourneyFactory, MessageFactory 14 14 from users.tests.factories import UserFactory 15 15 16 16 ··· 189 189 self.assertEqual(response.status_code, status.HTTP_200_OK) 190 190 response_data = json.loads(response.content.decode('utf-8')) 191 191 self.assertEquals(2, len(response_data['results'])) 192 + 193 + 194 + class MessageAPITest(APITestCase): 195 + 196 + def test_get_messages_journey(self): 197 + user = UserFactory() 198 + origin = ResidenceFactory(user=user) 199 + destination = CampusFactory() 200 + journey = JourneyFactory(user=user, residence=origin, campus=destination) 201 + [MessageFactory( 202 + user=UserFactory(), 203 + journey=JourneyFactory(user=UserFactory(), residence=origin, campus=destination)) 204 + for _ in range(2)] 205 + [MessageFactory(user=user, journey=journey) for _ in range(5)] 206 + [MessageFactory(user=UserFactory(), journey=journey) for _ in range(5)] 207 + url = "/api/v1/journeys/{}/messages/".format(journey.pk) 208 + self.client.force_authenticate(user=user) 209 + response = self.client.get(url) 210 + self.assertEqual(response.status_code, status.HTTP_200_OK) 211 + response_data = json.loads(response.content.decode('utf-8')) 212 + self.assertEquals(10, len(response_data['results'])) 213 + 214 + def test_not_allowed_messages(self): 215 + user = UserFactory() 216 + origin = ResidenceFactory(user=user) 217 + destination = CampusFactory() 218 + journey = JourneyFactory(user=user, residence=origin, campus=destination) 219 + [MessageFactory(user=user, journey=journey) for _ in range(5)] 220 + [MessageFactory(user=UserFactory(), journey=journey) for _ in range(5)] 221 + url = "/api/v1/journeys/{}/messages/".format(journey.pk) 222 + self.client.force_authenticate(user=UserFactory()) 223 + response = self.client.get(url) 224 + self.assertEqual(response.status_code, status.HTTP_200_OK) 225 + response_data = json.loads(response.content.decode('utf-8')) 226 + self.assertEquals(0, len(response_data['results'])) 227 + 228 + def test_create_message_owner_user(self): 229 + user = UserFactory() 230 + origin = ResidenceFactory(user=user) 231 + destination = CampusFactory() 232 + journey = JourneyFactory(user=user, residence=origin, campus=destination) 233 + self.assertEquals(0, journey.messages.count()) 234 + data = { 235 + "content": "Hello!", 236 + "journey": journey.pk, 237 + } 238 + self.client.force_authenticate(user=user) 239 + url = "/api/v1/messages/" 240 + response = self.client.post(url, data=data) 241 + self.assertEqual(response.status_code, status.HTTP_201_CREATED) 242 + self.assertEquals(1, journey.messages.count()) 243 + 244 + def test_get_messages(self): 245 + user = UserFactory() 246 + origin = ResidenceFactory(user=user) 247 + destination = CampusFactory() 248 + journey1 = JourneyFactory(user=user, residence=origin, campus=destination) 249 + journey2 = JourneyFactory(user=user, residence=origin, campus=destination) 250 + journey3 = JourneyFactory(user=UserFactory(), residence=origin, campus=destination) 251 + [MessageFactory(user=user, journey=journey1) for _ in range(2)] 252 + [MessageFactory(user=UserFactory(), journey=journey1) for _ in range(2)] 253 + [MessageFactory(user=user, journey=journey2) for _ in range(2)] 254 + [MessageFactory(user=UserFactory(), journey=journey2) for _ in range(2)] 255 + [MessageFactory(user=UserFactory(), journey=journey3) for _ in range(2)] 256 + url = "/api/v1/messages/" 257 + self.client.force_authenticate(user=user) 258 + response = self.client.get(url) 259 + self.assertEqual(response.status_code, status.HTTP_200_OK) 260 + response_data = json.loads(response.content.decode('utf-8')) 261 + self.assertEquals(8, len(response_data['results']))
+5 -4
upvcarshare/journeys/views.py
··· 11 11 12 12 from journeys import GOING 13 13 from journeys.exceptions import AlreadyAPassenger, NoFreePlaces, NotAPassenger 14 - from journeys.forms import JourneyForm, ResidenceForm, FilterForm, CancelJourneyForm 14 + from journeys.forms import JourneyForm, ResidenceForm, FilterForm, CancelJourneyForm, SmartJourneyForm 15 15 from journeys.models import Journey, Residence, Campus, Passenger 16 16 17 17 ··· 72 72 class CreateJourneyView(LoginRequiredMixin, View): 73 73 """View to show journey creation form and to handle its creation.""" 74 74 75 - template_name = "journeys/create.html" 75 + template_name = "journeys/create.smart.html" 76 + form = SmartJourneyForm 76 77 77 78 def get(self, request): 78 79 residences = Residence.objects.filter(user=request.user) ··· 83 84 "kind": GOING, 84 85 "departure": timezone.now().replace(second=0) 85 86 } 86 - form = JourneyForm(initial=initial, user=request.user) 87 + form = self.form(initial=initial, user=request.user) 87 88 data = { 88 89 "form": form 89 90 } 90 91 return render(request, self.template_name, data) 91 92 92 93 def post(self, request): 93 - form = JourneyForm(request.POST, user=request.user) 94 + form = self.form(request.POST, user=request.user) 94 95 data = { 95 96 "form": form 96 97 }
+1 -1
upvcarshare/notifications/__init__.py
··· 2 2 from __future__ import unicode_literals, print_function, absolute_import 3 3 4 4 # Notification verbs 5 - JOIN, LEAVE, CANCEL = "join", "leave", "cancel" 5 + JOIN, LEAVE, CANCEL, MESSAGE = "join", "leave", "cancel", "message"
+3 -1
upvcarshare/notifications/decorators.py
··· 22 22 """ 23 23 result = function(*args, **kwargs) 24 24 # Creates the notification after call the function. 25 - Notification.objects.create_from_method_call(verb=verb, function=function, args=args, kwargs=kwargs) 25 + Notification.objects.create_from_method_call( 26 + verb=verb, function=function, args=args, kwargs=kwargs, result=result 27 + ) 26 28 return result 27 29 return _wrapper_dispatch 28 30 return _decorator
+16
upvcarshare/notifications/helpers.py
··· 1 + # -*- coding: utf-8 -*- 2 + from __future__ import unicode_literals, print_function, absolute_import 3 + 4 + from journeys.models import Message 5 + from notifications import MESSAGE 6 + from notifications.models import Notification 7 + 8 + 9 + def dispatch_message(user, journey, message): 10 + """Helper to dispatch creation of message from API call.""" 11 + Notification.objects.create_from_method_call( 12 + verb=MESSAGE, function=Message.objects.send, args=tuple(), kwargs={ 13 + "user": user, 14 + "journey": journey, 15 + }, result=message 16 + )
+72 -22
upvcarshare/notifications/manager.py
··· 5 5 from django.db import models 6 6 from django.utils.functional import SimpleLazyObject 7 7 8 - from notifications import LEAVE, JOIN, CANCEL 8 + from notifications import LEAVE, JOIN, CANCEL, MESSAGE 9 9 10 10 11 11 def extract(classes, iterable): ··· 22 22 def unread(self, user): 23 23 return self.filter(user=user, read=False) 24 24 25 - def create_from_method_call(self, verb, function, args, kwargs): 25 + def _create_join_leave(self, **kwargs): 26 + """Creates notification for a join or leave journey.""" 27 + from journeys.models import Journey 28 + verb = kwargs.get("verb") 29 + objects = kwargs.get("objects") 30 + notification = self.model(verb=verb) 31 + actor = extract([get_user_model(), SimpleLazyObject], objects)[0] 32 + journey = extract(Journey, objects)[0] 33 + notification.user = journey.user 34 + notification.actor = actor 35 + notification.target = journey 36 + notification.save() 37 + return notification 38 + 39 + def _create_cancel(self, **kwargs): 40 + """Creates notification for a cancel journey.""" 41 + from journeys.models import Journey 42 + verb = kwargs.get("verb") 43 + objects = kwargs.get("objects") 44 + notifications = [] 45 + journey = extract(Journey, objects)[0] 46 + for passenger in journey.passengers.all(): 47 + notification = self.model(verb=verb) 48 + notification.actor = journey 49 + notification.user = passenger.user 50 + notification.save() 51 + notifications.append(notification) 52 + return notifications 53 + 54 + def _create_message(self, **kwargs): 55 + """Creates notification for a new message.""" 56 + from journeys.models import Journey 57 + verb = kwargs.get("verb") 58 + objects = kwargs.get("objects") 59 + result = kwargs.get("result") 60 + if result is None: 61 + try: 62 + journey = extract(Journey, objects)[0] 63 + actor = extract([get_user_model(), SimpleLazyObject], objects)[0] 64 + except KeyError: 65 + return None 66 + elif result.__class__.__name__ == "Message": 67 + journey = result.journey 68 + actor = result.user 69 + else: 70 + return None 71 + notifications = [] 72 + for passenger in journey.passengers.all(): 73 + if passenger.user != actor: 74 + notification = self.model(verb=verb) 75 + notification.actor = actor 76 + notification.user = passenger.user 77 + notification.target = journey 78 + notification.save() 79 + notifications.append(notification) 80 + if journey.user != actor: 81 + notification = self.model(verb=verb) 82 + notification.actor = actor 83 + notification.user = journey.user 84 + notification.target = journey 85 + notification.save() 86 + notifications.append(notification) 87 + return notifications 88 + 89 + def create_from_method_call(self, verb, function, args, kwargs, result=None): 26 90 """Creates a notification from a decorator call. 27 91 :param verb: 28 92 :param function: 29 93 :param args: 30 94 :param kwargs: 95 + :param result: 31 96 """ 32 - from journeys.models import Journey 33 - 97 + # Create objects 34 98 objects = list(args) + list(kwargs.values()) 35 - 36 99 # User ('actor') leaves|joins ('verb') journey ('target') 37 100 if verb in (JOIN, LEAVE): 38 - notification = self.model(verb=verb) 39 - actor = extract([get_user_model(), SimpleLazyObject], objects)[0] 40 - journey = extract(Journey, objects)[0] 41 - notification.user = journey.user 42 - notification.actor = actor 43 - notification.target = journey 44 - notification.save() 45 - return notification 101 + return self._create_join_leave(verb=verb, objects=objects) 46 102 elif verb == CANCEL: 47 - notifications = [] 48 - journey = extract(Journey, objects)[0] 49 - for passenger in journey.passengers.all(): 50 - notification = self.model(verb=verb) 51 - notification.actor = journey 52 - notification.user = passenger.user 53 - notification.save() 54 - notifications.append(notification) 55 - return notifications 103 + return self._create_cancel(verb=verb, objects=objects) 104 + elif verb == MESSAGE: 105 + return self._create_message(verb=verb, objects=objects, result=result) 56 106 return None
+7 -1
upvcarshare/notifications/models.py
··· 13 13 from django.utils.translation import ugettext_lazy as _ 14 14 from django_extensions.db.models import TimeStampedModel 15 15 16 - from notifications import JOIN, LEAVE, CANCEL 16 + from notifications import JOIN, LEAVE, CANCEL, MESSAGE 17 17 from notifications.manager import NotificationManager 18 18 19 19 ··· 75 75 value = _("El trayecto <strong>%(journey)s</strong> del %(date)s ha sido <strong>cancelado</strong>") % { 76 76 "journey": six.text_type(self.actor).lower(), 77 77 "date": localize(self.actor.departure), 78 + } 79 + elif self.verb == MESSAGE: 80 + value = _("%(user)s ha mandado un <strong>nuevo mensaje</strong> en <strong>%(journey)s</strong> del %(date)s") % { 81 + "user": six.text_type(self.actor), 82 + "journey": six.text_type(self.target).lower(), 83 + "date": localize(self.target.departure), 78 84 } 79 85 if strip_html: 80 86 value = strip_tags(value)
+14
upvcarshare/pages/views.py
··· 1 + # -*- coding: utf-8 -*- 2 + from __future__ import unicode_literals, print_function, absolute_import 3 + 4 + from django.core.urlresolvers import reverse 5 + from django.shortcuts import redirect 6 + from django.views.generic import View 7 + 8 + 9 + class HomeView(View): 10 + """Home view to redirect to recommended journeys.""" 11 + 12 + @staticmethod 13 + def get(request): 14 + return redirect(reverse("journeys:recommended"))
+7 -1
upvcarshare/static/src/js/app.js
··· 1 1 import jQuery from 'jquery'; 2 2 import Tether from 'tether'; 3 3 import angular from 'angular'; 4 + import Messenger from './messenger' 5 + import Journeys from './journeys' 6 + 4 7 5 8 // We define jQuery as global (using window object) and include Bootstrap 6 9 // JavaScript code. We use 'require' to avoid problems with npm bootstrap ··· 13 16 // General Angular 1.5 App 14 17 // ----------------------------------------------------------------------------- 15 18 angular 16 - .module('upvcarshare', []) 19 + .module('upvcarshare', [ 20 + Messenger.name, 21 + Journeys.name 22 + ]) 17 23 .config(['$httpProvider', '$interpolateProvider', ($httpProvider, $interpolateProvider) => { 18 24 $interpolateProvider.startSymbol('[[').endSymbol(']]'); 19 25 $httpProvider.defaults.xsrfCookieName = 'csrftoken';
+10
upvcarshare/static/src/js/journeys/index.js
··· 1 + import angular from 'angular'; 2 + import JourneyService from './journeys.service'; 3 + import OriginDestinationSelectComponent from './journeys.component'; 4 + 5 + const journeys = angular 6 + .module('journeys', []) 7 + .service('JourneyService', JourneyService) 8 + .component('originDestinationSelect', OriginDestinationSelectComponent); 9 + 10 + export default journeys;
+15
upvcarshare/static/src/js/journeys/journeys.component.js
··· 1 + // Component for select origin and destination of a journey. 2 + import controller from './journeys.controller'; 3 + 4 + 5 + const OriginDestinationSelectComponent = { 6 + controller, 7 + templateUrl: "/partials/journeys/origin_destiny_select.html", 8 + bindings: { 9 + originField: '@', 10 + destinyField: '@' 11 + } 12 + }; 13 + 14 + 15 + export default OriginDestinationSelectComponent;
+86
upvcarshare/static/src/js/journeys/journeys.controller.js
··· 1 + class OriginDestinationSelectController { 2 + 3 + constructor($scope, JourneyService) { 4 + this.$scope = $scope; 5 + this.journeyService = JourneyService; 6 + } 7 + 8 + $onInit() { 9 + this.residences = []; 10 + this.campus = []; 11 + 12 + this.originOptions = []; 13 + this.originSelected = null; 14 + this.originFieldValue = ""; 15 + 16 + this.destinyOptions = []; 17 + this.destinySelected = null; 18 + this.destinyFieldValue = ""; 19 + 20 + Promise.all(this.loadData()).then( () => { 21 + this.$scope.$apply( () => { 22 + this.buildOriginOptions(); 23 + }); 24 + }); 25 + 26 + } 27 + 28 + // Load data 29 + loadData() { 30 + return [ 31 + // Load residences 32 + this.journeyService.getResidences().then( response => { 33 + this.residences = response.results; 34 + }), 35 + // Load campus 36 + this.journeyService.getCampus().then( response => { 37 + this.campus = response.results; 38 + }) 39 + ]; 40 + } 41 + 42 + // Builds the list of available options for origin 43 + buildOriginOptions() { 44 + if (this.originSelected === null) { 45 + // If there is not origin, destination is disabled... 46 + this.originOptions = this.residences.concat(this.campus); 47 + } 48 + } 49 + 50 + changeOrigin() { 51 + if (this.campus.indexOf(this.originSelected) != -1) { 52 + this.originFieldValue = "campus:" + this.originSelected.id; 53 + } else if (this.residences.indexOf(this.originSelected) != -1) { 54 + this.originFieldValue = "residence:" + this.originSelected.id; 55 + } 56 + this.buildDestinyOptions(); 57 + } 58 + 59 + changeDestiny() { 60 + if (this.campus.indexOf(this.destinySelected) != -1) { 61 + this.destinyFieldValue = "campus:" + this.destinySelected.id; 62 + } else if (this.residences.indexOf(this.originSelected) != -1) { 63 + this.destinyFieldValue = "residence:" + this.destinySelected.id; 64 + } 65 + } 66 + 67 + // Builds the list of available options for destiny 68 + buildDestinyOptions() { 69 + if (this.campus.indexOf(this.originSelected) != -1) { 70 + // If there is selected a residence, only options for destiny are campus 71 + this.destinyOptions = this.residences; 72 + } else if (this.residences.indexOf(this.originSelected) != -1) { 73 + // If there is selected a residence, only options for destiny are residendes 74 + this.destinyOptions = this.campus; 75 + } 76 + if (this.destinyOptions.length > 0) { 77 + this.destinySelected = this.destinyOptions[0]; 78 + this.changeDestiny(); 79 + } 80 + } 81 + 82 + } 83 + 84 + OriginDestinationSelectController.$inject = ['$scope', 'JourneyService']; 85 + 86 + export default OriginDestinationSelectController;
+23
upvcarshare/static/src/js/journeys/journeys.service.js
··· 1 + // JourneyService to access services API. 2 + 3 + 4 + class JourneyService { 5 + 6 + constructor($http) { 7 + this.$http = $http; 8 + } 9 + 10 + getResidences() { 11 + return this.$http.get("/api/v1/residences/") 12 + .then(response => response.data ); 13 + } 14 + 15 + getCampus() { 16 + return this.$http.get("/api/v1/campus/") 17 + .then(response => response.data ); 18 + } 19 + } 20 + 21 + JourneyService.$inject = ['$http']; 22 + 23 + export default JourneyService;
+14
upvcarshare/static/src/js/messenger/index.js
··· 1 + import angular from 'angular'; 2 + import MessengerService from './messenger.service' 3 + import {MessengerComponent, MessageListComponent, MessageFormComponent} from './messenger.component'; 4 + 5 + 6 + // Messenger module 7 + const messenger = angular 8 + .module('messenger', []) 9 + .service('MessengerService', MessengerService) 10 + .component('messenger', MessengerComponent) 11 + .component('messageList', MessageListComponent) 12 + .component('messageForm', MessageFormComponent); 13 + 14 + export default messenger;
+35
upvcarshare/static/src/js/messenger/messenger.component.js
··· 1 + import {MessengerController, MessageListController, MessageFormController} from './messenger.controller' 2 + 3 + 4 + // Component that encapsulate all messenger functionality 5 + const MessengerComponent = { 6 + controller: MessengerController, 7 + templateUrl: "/partials/messenger/messenger.html", 8 + bindings: { 9 + journey: '@', 10 + firstName: '@', 11 + lastName: '@' 12 + } 13 + }; 14 + 15 + // Component that shows a single message 16 + const MessageListComponent = { 17 + controller: MessageListController, 18 + templateUrl: "/partials/messenger/message.list.html", 19 + bindings: { 20 + messages: '<' 21 + } 22 + }; 23 + 24 + // Component to show a form to create messages 25 + const MessageFormComponent = { 26 + controller: MessageFormController, 27 + templateUrl: "/partials/messenger/message.form.html", 28 + bindings: { 29 + message: '<', 30 + onSendMessage: '&' 31 + } 32 + }; 33 + 34 + 35 + export {MessageListComponent, MessageFormComponent, MessengerComponent};
+114
upvcarshare/static/src/js/messenger/messenger.controller.js
··· 1 + import moment from 'moment'; 2 + moment.locale('es'); 3 + 4 + 5 + class MessengerController { 6 + 7 + constructor($scope, MessengerService) { 8 + this.messengerService = MessengerService; 9 + this.$scope = $scope; 10 + } 11 + 12 + $onInit() { 13 + this.messages = []; 14 + this.newMessage = this.getNewMessage(); 15 + this.savingMessage = false; 16 + this.loadingMessages = false; 17 + 18 + this.loadMessages(); 19 + } 20 + 21 + // Launch initial messages 22 + loadMessages() { 23 + this.loadingMessages = true; 24 + this.messengerService.getMessages(this.journey).then( response => { 25 + this.messages = response.results; 26 + if (!response.next) { 27 + this.loadingMessages = false; 28 + } else { 29 + this.paginateMessages(response.next); 30 + } 31 + }); 32 + } 33 + 34 + // Method to resolve pagination of messages 35 + paginateMessages(url) { 36 + this.messengerService.getFromUrl(url).then( response => { 37 + response.results.forEach( (value) => { 38 + this.messages.push(value); 39 + }); 40 + if (!response.next) { 41 + this.loadingMessages = false; 42 + } else { 43 + this.paginateMessages(response.next); 44 + } 45 + }); 46 + } 47 + 48 + // Creates a new message 49 + getNewMessage() { 50 + return { 51 + journey: this.journey, 52 + content: "", 53 + user: { 54 + first_name: this.firstName, 55 + last_name: this.lastName 56 + }, 57 + created: moment().toISOString() 58 + }; 59 + } 60 + 61 + // Sends a message 62 + sendMessage({message}) { 63 + if (!message || this.loadingMessages || this.savingMessage) return; 64 + this.messages.push(message); 65 + this.savingMessage = true; 66 + this.messengerService.postMessage(message.journey, message.content).then( response => { 67 + this.savingMessage = false; 68 + }); 69 + this.newMessage = this.getNewMessage(); 70 + } 71 + 72 + } 73 + MessengerController.$inject = ['$scope', 'MessengerService']; 74 + 75 + 76 + class MessageListController { 77 + 78 + constructor() {} 79 + 80 + showTimestamp(timeString) { 81 + return moment(timeString).calendar(); 82 + } 83 + 84 + } 85 + 86 + 87 + class MessageFormController { 88 + 89 + constructor() {} 90 + 91 + // The $onChanges lifecycle hook makes a clone of the initial this.message 92 + // binding Object and reassigns it, which means the parent data is not 93 + // affected until we submit the form, alongside one-way data flow new 94 + // binding syntax '<'. 95 + $onChanges(changes) { 96 + if (changes.message) { 97 + this.message = Object.assign({}, this.message); 98 + } 99 + } 100 + 101 + // Pass the message to the messenger controller 102 + onSubmit() { 103 + if (!this.message.content) return; 104 + this.onSendMessage({ 105 + $event: { 106 + message: this.message 107 + } 108 + }); 109 + } 110 + 111 + } 112 + 113 + 114 + export {MessageFormController, MessageListController, MessengerController}
+35
upvcarshare/static/src/js/messenger/messenger.service.js
··· 1 + // Class based service to access the API of messenger. 2 + class MessengerService { 3 + 4 + constructor($http) { 5 + this.$http = $http; 6 + } 7 + 8 + // Gets the list of messages for a given journey 9 + getMessages(journey) { 10 + return this.$http.get(`/api/v1/journeys/${journey}/messages/`) 11 + .then(response => response.data ); 12 + } 13 + 14 + // Use this method to paginate requests 15 + getFromUrl(url) { 16 + return this.$http.get(url) 17 + .then(response => response.data ); 18 + } 19 + 20 + // Post a message 21 + postMessage(journey, content) { 22 + return this.$http.post( 23 + "/api/v1/messages/", 24 + {journey, content}, 25 + { 26 + headers: { 27 + "X-CSRFToken": $('input[name=csrfmiddlewaretoken]').val() 28 + } 29 + } 30 + ).then(response => response.data ); 31 + } 32 + 33 + } 34 + 35 + export default MessengerService;
+4
upvcarshare/static/src/sass/journeys.scss
··· 22 22 border-top: $table-border-width dotted $table-border-color; 23 23 } 24 24 } 25 + 26 + #id_i_am_driver { 27 + @extend .list-unstyled; 28 + }
+12
upvcarshare/static/src/sass/messenger.scss
··· 1 + // Styles for messenger module 2 + @import "variables"; 3 + 4 + 5 + .messenger { 6 + .message { 7 + padding: 5px; 8 + background-color: #eee; 9 + border-bottom: 1px dotted $upv-dark-grey; 10 + margin-bottom: 10px; 11 + } 12 + }
+1
upvcarshare/static/src/sass/project.scss
··· 14 14 @import "journeys"; 15 15 @import "colors"; 16 16 @import "notifications"; 17 + @import "messenger"; 17 18 18 19 html, body{ 19 20 height:100%;
+7 -5
upvcarshare/templates/header.html
··· 1 1 {% extends "base.html" %} 2 2 {% load i18n %} 3 3 {% load static %} 4 + {% load core_tags %} 5 + 4 6 {% block header %} 5 7 <div class="container section-ribbon"> 6 8 <div class="row"> ··· 26 28 27 29 {% if request.user.is_authenticated %} 28 30 <ul class="nav navbar-nav pull-xs-left"> 29 - <li class="nav-item"> 31 + <li class="nav-item {% add_active_class "notifications:list" %}"> 30 32 <a class="nav-link" href="{% url "notifications:list" %}"> 31 33 {% trans "Notificaciones" %} 32 34 {% if unread_notifications > 0 %} ··· 36 38 </li> 37 39 </ul> 38 40 <ul class="nav navbar-nav pull-xs-right"> 39 - <li class="nav-item"> 40 - <a class="nav-link" href="{% url "journeys:recommended" %}">{% trans "Buscador" %}</a> 41 + <li class="nav-item {% add_active_class "journeys:recommended" %}"> 42 + <a class="nav-link" href="{% url "journeys:recommended" %}">{% trans "Recomendaciones" %}</a> 41 43 </li> 42 - <li class="nav-item dropdown"> 44 + <li class="nav-item {% add_active_class "journeys:user-list,journeys:passenger,journeys:residences" %} dropdown"> 43 45 <a class="nav-link dropdown-toggle" 44 46 data-toggle="dropdown" 45 47 href="#" ··· 55 57 <a class="dropdown-item" href="{% url "journeys:residences" %}">{% trans "Mis lugares" %}</a> 56 58 </div> 57 59 </li> 58 - <li class="nav-item dropdown"> 60 + <li class="nav-item {% add_active_class "users:edit" %} dropdown"> 59 61 <a class="nav-link dropdown-toggle" 60 62 data-toggle="dropdown" 61 63 href="#"
-1
upvcarshare/templates/journeys/create.html
··· 23 23 <div class="form-group row{% if field.errors %} has-danger{% endif %}"> 24 24 <label for="{{ field.auto_id }}" class="col-sm-4 form-control-label">{{ field.label }}</label> 25 25 <div class="col-sm-8"> 26 - {# TODO Create a widget specifc #} 27 26 {% if field.name == "residence" %} 28 27 <div class="row"> 29 28 <div class="col-sm-7">
+69
upvcarshare/templates/journeys/create.smart.html
··· 1 + {% extends "header.html" %} 2 + 3 + {% load i18n %} 4 + 5 + {% block extra_head %} 6 + {{ form.media }} 7 + {% endblock %} 8 + 9 + {% block section_title %} 10 + <div class="row"> 11 + <div class="col-xs-12"> 12 + <h2>{% trans "Crear trayecto" %}</h2> 13 + </div> 14 + </div> 15 + {% endblock section_title %} 16 + 17 + {% block section_messages %} 18 + {% if form.errors %} 19 + {{ form.erros }} 20 + {% endif %} 21 + {% endblock section_messages %} 22 + 23 + {% block content %} 24 + <div class="row"> 25 + <div class="col-xs-12"> 26 + <form method="post" action=""> 27 + {% csrf_token %} 28 + <origin-destination-select 29 + origin-field="{{ form.origin.name }}" 30 + origin-field-di="{{ form.origin.auto_id }}" 31 + destiny-field="{{ form.destiny.name }}" 32 + destiny-field-id="{{ form.destiny.auto_id }}"> 33 + </origin-destination-select> 34 + {% for field in form %} 35 + {% if not field.is_hidden %} 36 + <div class="form-group row{% if field.errors %} has-danger{% endif %}"> 37 + <label for="{{ field.auto_id }}" class="col-sm-4 form-control-label">{{ field.label }}</label> 38 + <div class="col-sm-8"> 39 + {% if field.name == "residence" %} 40 + <div class="row"> 41 + <div class="col-sm-7"> 42 + {{ field }} 43 + </div> 44 + <div class="col-sm-5"> 45 + {% block residence_action %} 46 + <div class="btn-group pull-xs-right" role="group" aria-label="{% trans "Acciones" %}"> 47 + <a href="{% url "journeys:create-residence" %}" class="btn btn-success-outline">{% trans "Crear nuevo lugar" %}</a> 48 + </div> 49 + {% endblock %} 50 + </div> 51 + </div> 52 + {% else %} 53 + {{ field }} 54 + {% endif %} 55 + <span class="text-muted">{{ field.help_text }}</span> 56 + <span class="text-muted">{{ field.errors }}</span> 57 + </div> 58 + </div> 59 + {% endif %} 60 + {% endfor %} 61 + <div class="form-group"> 62 + <div class="col-sm-offset-4 col-sm-8"> 63 + <button type="submit" class="btn btn-success">{% trans "Guardar" %}</button> 64 + </div> 65 + </div> 66 + </form> 67 + </div> 68 + </div> 69 + {% endblock content %}
+6 -4
upvcarshare/templates/journeys/details.html
··· 18 18 {% if request.user == journey.user %} 19 19 <a href="{% url "journeys:edit" journey.pk %}" class="btn btn-success" style="margin-bottom: 5px">{% trans "Editar" %}</a> 20 20 {% if not journey.needs_driver and not journey.disabled %} 21 - <a href="{% url "journeys:cancel" journey.pk %}" class="btn btn-danger">{% trans "Cancelar trayecto" %}</a> 21 + <a href="{% url "journeys:cancel" journey.pk %}" class="btn btn-danger">{% trans "Cancelar trayecto" %}</a> 22 22 {% endif %} 23 23 {% else %} 24 24 {% journey_join_leave_button journey %} ··· 27 27 28 28 {% block content %} 29 29 <div class="row"> 30 - <div class="col-sm-9"> 30 + <div class="col-sm-8"> 31 31 {% if not journey.needs_driver %} 32 32 <h3>{% trans "Datos del transporte" %}</h3> 33 33 <table class="table"> ··· 57 57 <h4 class="text-muted">{% trans "No se ha encontrado transporte recomendado" %}</h4> 58 58 {% endfor %} 59 59 {% endif %} 60 + <h3>{% trans "Tablón de mensajes" %}</h3> 61 + <messenger journey="{{ journey.pk }}" first-name="{{ request.user.first_name }}" last-name="{{ request.user.last_name }}"></messenger> 62 + </div> 63 + <div class="col-sm-4"> 60 64 {% if show_passengers %} 61 65 <h3>{% trans "Pasajeros" %}</h3> 62 66 <table class="table"> ··· 81 85 </tbody> 82 86 </table> 83 87 {% endif %} 84 - </div> 85 - <div class="col-sm-3"> 86 88 <h3> 87 89 {% if journey.kind == 0 %} 88 90 {% trans "Origen" %}
+3 -1
upvcarshare/templates/journeys/recommended.html
··· 35 35 {% for journey in journeys %} 36 36 {% journey_item journey %} 37 37 {% empty %} 38 - <h3 class="text-muted">{% trans "No trayectos recomendados" %}</h3> 38 + <h3 class="text-muted">{% trans "No hemos encontrado trayectos recomendados" %}</h3> 39 + <p class="lead">{% trans "Puedes crear los trayectos en los que necesitas transporte para que te mostremos aquí una lista de conducotres que pueden llevarte a tu destino." %}</p> 40 + <a href="{% url "journeys:create" %}" class="btn btn-lg btn-primary">{% trans "Crear trayecto" %}</a> 39 41 {% endfor %} 40 42 </div> 41 43 </div>
+40
upvcarshare/templates/partials/journeys/origin_destiny_select.html
··· 1 + {% load i18n %} 2 + <div class="form-group row"> 3 + <label class="col-sm-4 form-control-label">{% trans "Lugar de origen" %}</label> 4 + <div class="col-sm-8"> 5 + <div class="row"> 6 + <div class="col-sm-7"> 7 + <select class="form-control" 8 + ng-model="$ctrl.originSelected" 9 + ng-change="$ctrl.changeOrigin()" 10 + ng-options="option.name for option in $ctrl.originOptions"> 11 + </select> 12 + <input type="hidden" name="[[$ctrl.originField]]" id="[[$ctrl.originFieldId]]" value="[[$ctrl.originFieldValue]]" > 13 + </div> 14 + <div class="col-sm-5"> 15 + {% block residence_action %} 16 + <div class="btn-group pull-xs-right" role="group" aria-label="{% trans "Acciones" %}"> 17 + <a href="{% url "journeys:create-residence" %}" class="btn btn-success-outline">{% trans "Crear nuevo lugar" %}</a> 18 + </div> 19 + {% endblock %} 20 + </div> 21 + </div> 22 + </div> 23 + </div> 24 + <div class="form-group row"> 25 + <label class="col-sm-4 form-control-label">{% trans "Lugar de destino" %}</label> 26 + <div class="col-sm-8"> 27 + <div class="row"> 28 + <div class="col-sm-7"> 29 + <select class="form-control" 30 + 31 + ng-disabled="$ctrl.originSelected == null" 32 + ng-change="$ctrl.changeDestiny()" 33 + ng-model="$ctrl.destinySelected" 34 + ng-options="option.name for option in $ctrl.destinyOptions"> 35 + </select> 36 + <input type="hidden" name="[[$ctrl.destinyField]]" id="[[$ctrl.destinyFieldId]]" value="[[$ctrl.destinyFieldValue]]" > 37 + </div> 38 + </div> 39 + </div> 40 + </div>
+14
upvcarshare/templates/partials/messenger/message.form.html
··· 1 + {% load i18n %} 2 + <form name="messageForm" ng-submit="$ctrl.onSubmit();"> 3 + {% csrf_token %} 4 + <div class="row"> 5 + <div class="col-xs-10"> 6 + <textarea class="form-control" ng-model="$ctrl.message.content"></textarea> 7 + </div> 8 + <div class="col-xs-2"> 9 + <button type="submit" class="btn btn-primary">{% trans "Enviar" %}</button> 10 + </div> 11 + </div> 12 + </form> 13 + 14 +
+15
upvcarshare/templates/partials/messenger/message.list.html
··· 1 + <div class="message" ng-repeat="message in $ctrl.messages"> 2 + <div class="row"> 3 + <div class="col-xs-12"> 4 + <div class="row"> 5 + <div class="col-sm-10"> 6 + <h5 class="text-danger">[[message.user.first_name]] [[message.user.last_name]]</h5> 7 + </div> 8 + <div class="col-sm-2"> 9 + <small>[[$ctrl.showTimestamp(message.created)]]</small> 10 + </div> 11 + </div> 12 + <p class="content">[[ message.content ]]</p> 13 + </div> 14 + </div> 15 + </div>
+7
upvcarshare/templates/partials/messenger/messenger.html
··· 1 + <div class="row messenger"> 2 + <div class="col-sm-12"> 3 + <message-list messages="$ctrl.messages"></message-list> 4 + <message-form message="$ctrl.newMessage" 5 + on-send-message="$ctrl.sendMessage($event);"></message-form> 6 + </div> 7 + </div>