', { 'class': 'faq-toc' });
152 |
153 | $toc.html (html);
154 |
155 | return $toc;
156 |
157 | }
158 | };
159 | })(jQuery);
--------------------------------------------------------------------------------
/dev_static/site/js/guidely/guidely-number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/dev_static/site/js/guidely/guidely-number.png
--------------------------------------------------------------------------------
/dev_static/site/js/guidely/guidely.css:
--------------------------------------------------------------------------------
1 | .guidely-number {
2 | background: url(guidely-number.png) no-repeat 0 0;
3 | width: 45px;
4 | height: 45px;
5 | display: none;
6 | position: absolute;
7 | cursor: pointer;
8 | z-index: 10002;
9 | }
10 |
11 | .guidely-number span {
12 | width: 43px;
13 | height: 43px;
14 | font-family: arial, sans-serif;
15 | font-size: 20px;
16 | font-weight: bold;
17 | text-align: center;
18 | color: #FFF;
19 | text-align: center;
20 | display: block;
21 | line-height: 44px;
22 | }
23 |
24 | .guidely-guide {
25 | background: #FFF;
26 | width: 300px;
27 | display: none;
28 | border: 3px solid #999;
29 |
30 | -webkit-border-radius:5px;
31 | -moz-border-radius:5px;
32 | border-radius:5px;
33 |
34 | -webkit-box-shadow:0 0 12px rgba(0,0,0,0.4);
35 | -moz-box-shadow:0 0 12px rgba(0,0,0,0.4);
36 | box-shadow:0 0 12px rgba(0,0,0,0.4);
37 |
38 | z-index: 10001;
39 | }
40 |
41 | .guidely-guide h4 {
42 | font-family: Helvetica, arial, sans-serif;
43 | font-size: 15px;
44 | font-weight: bold;
45 | color: #333;
46 | padding-bottom: 15px !important;
47 | padding: 0;
48 | margin: 0 0 1em;
49 | border-bottom: 1px dotted #CCC;
50 | }
51 |
52 | .guidely-guide-pad {
53 | font-size: 12px;
54 | line-height: 1.7em;
55 | padding: 15px 15px 5px 30px;
56 |
57 | }
58 |
59 | .guidely-anchor-right .guidely-guide-pad { padding: 15px 30px 5px 15px; }
60 |
61 | .guidely-anchor-right .guidely-close-trigger { right: 30px; }
62 |
63 | .guidely-popup
64 | {
65 | color: #444;
66 | display:block;
67 | padding: 0;
68 | background: #fff;
69 |
70 | -webkit-border-top-left-radius: 4px;
71 | -webkit-border-top-right-radius: 4px;
72 | -moz-border-radius-topleft: 4px;
73 | -moz-border-radius-topright: 4px;
74 | border-top-left-radius: 4px;
75 | border-top-right-radius: 4px;
76 | }
77 |
78 |
79 |
80 |
81 |
82 | .guidely-controls {
83 | background: #EEE;
84 | text-align: right;
85 | padding: 7px 10px;
86 | margin-top: 1em;
87 | }
88 |
89 | .guidely-controls button {
90 | font-size: 11px;
91 | padding: 3px 8px;
92 | *padding: 1px 4px;
93 | cursor: pointer;
94 | }
95 |
96 | .guidely-overlay
97 | {
98 | position: fixed;
99 | top: 0px;
100 | left: 0px;
101 | height:100%;
102 | width:100%;
103 | background-color: #000;
104 | z-index: 10000;
105 |
106 | filter: alpha(opacity=30);
107 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=30);
108 | -moz-opacity: 0.30;
109 | opacity:0.30;
110 | }
111 |
112 | .guidely-start-trigger {
113 | background: #444;
114 | background: rgba(0,0,0,.6);
115 | text-decoration: none;
116 | color: #FFF;
117 | cursor: pointer;
118 | padding: 4px 10px 4px 12px;
119 | position: fixed;
120 | top: 0;
121 | right: 0;
122 |
123 | z-index: 9999;
124 |
125 | -webkit-border-bottom-left-radius: 5px;
126 | -moz-border-radius-bottomleft: 5px;
127 | border-bottom-left-radius: 5px;
128 |
129 | }
130 |
131 | .guidely-close-trigger {
132 | font-family: Helvetica, arial, sans-serif;
133 | font-size: 13px;
134 | font-weight: bold;
135 | text-decoration: none;
136 | color: #AAA;
137 | position: absolute;
138 | right:16px;
139 | top: 12px;
140 | }
141 |
142 | #guide-welcome { width: 350px; }
143 |
144 | #guide-welcome .guidely-guide-pad { padding: 15px 15px 5px 15px; }
--------------------------------------------------------------------------------
/dev_static/site/js/pushetta.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Hook delle richieste ajax per gestire il csfr token
3 | */
4 | $(document).ajaxSend(function(event, xhr, settings) {
5 | function getCookie(name) {
6 | var cookieValue = null;
7 | if (document.cookie && document.cookie != '') {
8 | var cookies = document.cookie.split(';');
9 | for (var i = 0; i < cookies.length; i++) {
10 | var cookie = jQuery.trim(cookies[i]);
11 | // Does this cookie string begin with the name we want?
12 | if (cookie.substring(0, name.length + 1) == (name + '=')) {
13 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
14 | break;
15 | }
16 | }
17 | }
18 | return cookieValue;
19 | }
20 | function sameOrigin(url) {
21 | // url could be relative or scheme relative or absolute
22 | var host = document.location.host; // host + port
23 | var protocol = document.location.protocol;
24 | var sr_origin = '//' + host;
25 | var origin = protocol + sr_origin;
26 | // Allow absolute or scheme relative URLs to same origin
27 | return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
28 | (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
29 | // or any other URL that isn't scheme relative or absolute i.e relative.
30 | !(/^(\/\/|http:|https:).*/.test(url));
31 | }
32 | function safeMethod(method) {
33 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
34 | }
35 |
36 | if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
37 | xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
38 | }
39 | });
40 |
41 | function generateUUID() {
42 | var d = new Date().getTime();
43 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
44 | var r = (d + Math.random() * 16) % 16 | 0;
45 | d = Math.floor(d / 16);
46 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
47 | });
48 | return uuid;
49 | }
50 |
51 | function getQueryParameterByName(name) {
52 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
53 | var regex = new RegExp("[\\?&]" + name + "=([^]*)"),
54 | results = regex.exec(location.search);
55 | return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
56 | }
--------------------------------------------------------------------------------
/dev_static/site/js/signin.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 |
3 | jQuery.support.placeholder = false;
4 | test = document.createElement('input');
5 | if('placeholder' in test) jQuery.support.placeholder = true;
6 |
7 | if (!$.support.placeholder) {
8 |
9 | $('.field').find ('label').show ();
10 |
11 | }
12 |
13 | });
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | pushetta:
5 | container_name: pushetta-be
6 | command: python pushetta/manage.py runserver 0.0.0.0:8001
7 | build:
8 | context: .
9 | expose:
10 | - 8001
11 | ports:
12 | - 8001:8001
13 | depends_on:
14 | - db
15 | - redis
16 | - elastic
17 | env_file:
18 | - ./pushetta.env
19 | volumes:
20 | - ./volumes/pushetta:/usr/share/pushetta/
21 |
22 | db:
23 | image: mysql:5.5
24 | container_name: db
25 | ports:
26 | - 3306:3306
27 | expose:
28 | - 3306
29 | env_file:
30 | - ./database.env
31 | volumes:
32 | - ./volumes/database/:/var/lib/mysql/
33 |
34 | elastic:
35 | image: elasticsearch:2
36 | container_name: elastic
37 | ports:
38 | - '9200:9200'
39 | expose:
40 | - 9200
41 | environment:
42 | - http.host=0.0.0.0
43 | - transport.host=127.0.0.1
44 | - bootstrap.memory_lock=true
45 | - 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
46 | cap_add:
47 | - IPC_LOCK
48 | volumes:
49 | - ./volumes/elasticsearch:/usr/share/elasticsearch/data
50 |
51 | redis:
52 | image: redis:4.0-alpine
53 | container_name: redis
54 | ports:
55 | - 6379:6379
56 | expose:
57 | - 6379
58 | volumes:
59 | - ./volumes/redis:/data
60 |
61 | mosquitto:
62 | image: guglielmino/mosquitto-pushetta-auth-plugin:v1.0.9
63 | container_name: mosquitto
64 | ports:
65 | - 1883:1883
66 | expose:
67 | - 1883
68 | depends_on:
69 | - db
70 | command:
71 | ['/wait_for', 'db:3306', '--', '/usr/sbin/mosquitto', '-c', '/etc/mosquitto/mosquitto.conf']
72 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | set -e
4 |
5 | mkdir -p /usr/src/app/log/
6 |
7 | if [ "x$DJANGO_MANAGEPY_MIGRATE" = 'xon' ]; then
8 | python pushetta/manage.py migrate --noinput
9 | fi
10 |
11 | if [ "x$DJANGO_MANAGEPY_COLLECTSTATIC" = 'xon' ]; then
12 | python pushetta/manage.py collectstatic --noinput
13 | fi
14 |
15 | exec "$@"
--------------------------------------------------------------------------------
/docker_push:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
3 | docker push guglielmino/pushetta-api:$1
4 |
--------------------------------------------------------------------------------
/pushetta/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/api/__init__.py
--------------------------------------------------------------------------------
/pushetta/api/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.contrib import admin
5 |
6 | # Register your models here.
7 |
--------------------------------------------------------------------------------
/pushetta/api/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class ApiConfig(AppConfig):
8 | name = 'api'
9 |
--------------------------------------------------------------------------------
/pushetta/api/crashes_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le funzionalità per gestire i crashlog delle App (Android inizialmente)
5 |
6 | from django.conf import settings
7 | import logging
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | from rest_framework import generics
12 | from rest_framework.response import Response
13 | from rest_framework import status
14 |
15 |
16 | class CrashLogService(generics.GenericAPIView):
17 | def post(self, request, format=None):
18 | logger.error(str(request.DATA))
19 | return Response(status=status.HTTP_200_OK)
20 |
--------------------------------------------------------------------------------
/pushetta/api/feedback_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le funzionalità per feedback letture
5 |
6 | import logging
7 | logger = logging.getLogger(__name__)
8 |
9 | from rest_framework import generics
10 | from rest_framework.response import Response
11 | from rest_framework import status
12 |
13 | from api.serializers import FeedbackSerializer
14 | from core.feedback_manager import FeedbackManager
15 | from core.models import Channel
16 | from api.permissions import IsDeviceCallAuthorized
17 | from core.services import set_read_feedback_multiple
18 |
19 |
20 | class FeedbackService(generics.GenericAPIView):
21 | serializer_class = FeedbackSerializer
22 |
23 | permission_classes = [
24 | IsDeviceCallAuthorized
25 | ]
26 |
27 | def post(self, request, format=None, messages_id=None):
28 | serializer = FeedbackSerializer(data=request.DATA)
29 | if not serializer.is_valid():
30 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
31 |
32 |
33 | feedback_dict = serializer.object
34 | set_read_feedback_multiple(feedback_dict["device_id"], feedback_dict["messages_id"])
35 | return Response(status=status.HTTP_201_CREATED)
36 |
37 |
--------------------------------------------------------------------------------
/pushetta/api/messages_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le funzionalità dei messaggi storati
5 |
6 | from datetime import datetime
7 |
8 | from rest_framework import generics
9 | from rest_framework.response import Response
10 | from rest_framework import status
11 | from django.shortcuts import get_object_or_404
12 | from django.db.models import Q
13 |
14 | from api.serializers import ChannelMsgSerializer
15 | from core.subscriber_manager import SubscriberManager
16 | from core.models import ChannelMsg
17 | from api.permissions import IsDeviceCallAuthorized
18 |
19 |
20 | class MessageList(generics.GenericAPIView):
21 | """
22 | Get a single message or a messages list belonging to
23 | calling device
24 | message_id -- Message identifier
25 | """
26 |
27 | serializer_class = ChannelMsgSerializer
28 |
29 | permission_classes = [
30 | IsDeviceCallAuthorized
31 | ]
32 |
33 | # NOTA: Va definito un meccanismo di protezione per la get dei messaggi di un device per evitare
34 | # che conoscendo il device_id di un utente si possano acquisire i messaggi (problema reale solo
35 | # per i canali privati, quelli pubblici hanno messaggi visibili a tutti per definizione)
36 | def get(self, request, format=None, message_id=None, device_id=None):
37 |
38 | if message_id != None:
39 | obj = get_object_or_404(ChannelMsg, pk=message_id)
40 | serializer = ChannelMsgSerializer(obj)
41 |
42 | return Response(serializer.data, status=status.HTTP_200_OK)
43 |
44 | if device_id != None:
45 | channel_names = SubscriberManager().get_device_subscriptions(device_id)
46 | # Nota: verificare come gestire la casistica di risultati molto ampi, introdotto inanto il throttling per limitare
47 | messages = ChannelMsg.objects.filter(Q(expire__isnull=True) | Q(expire__gte=datetime.utcnow())).filter(
48 | channel__name__in=channel_names)
49 | serializer = ChannelMsgSerializer(messages, many=True)
50 |
51 | return Response(serializer.data, status=status.HTTP_200_OK)
52 |
53 | return Response(status=status.HTTP_404_NOT_FOUND)
54 |
55 |
--------------------------------------------------------------------------------
/pushetta/api/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 |
6 | # Create your models here.
7 |
--------------------------------------------------------------------------------
/pushetta/api/permissions.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Permission custom
5 |
6 | from rest_framework import permissions
7 |
8 |
9 | class IsDeviceCallAuthorized(permissions.BasePermission):
10 | """
11 | Custom permission to check if a device can invoke a API method.
12 | It check a custom HTTP Header for operations made from devices (it check a signature based on seed delivered with push notification)
13 | Custom HTTP header is X-Auth-Token
14 | """
15 |
16 | def has_permission(self, request, view):
17 | if 'HTTP_X_AUTH_TOKEN' in request.META:
18 | req_custom_header = request.META['HTTP_X_AUTH_TOKEN']
19 | print req_custom_header
20 |
21 | return True
22 |
23 | def has_object_permission(self, request, view, obj):
24 | return True
25 |
26 |
27 | class IsChannelOwner(permissions.BasePermission):
28 | """
29 | Object-level permission to only allow owners of an object to edit it.
30 | Assumes the model instance has an `owner` attribute.
31 | """
32 |
33 | def has_permission(self, request, view):
34 | print "IsChannelOwner has_permission"
35 | return True
36 |
37 | def has_object_permission(self, request, view, obj):
38 | print "IsChannelOwner has_object_permission"
39 | # Read permissions are allowed to any request,
40 | # so we'll always allow GET, HEAD or OPTIONS requests.
41 | if request.method in permissions.SAFE_METHODS:
42 | return True
43 |
44 | # Instance must have an attribute named `owner`.
45 | return obj.owner == request.user
--------------------------------------------------------------------------------
/pushetta/api/publisher_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le funzionalità per la gestione Publishers
5 |
6 | from rest_framework import generics
7 |
8 | from api.serializers import PublisherSerializer
9 |
10 |
11 |
12 |
13 | class PublisherList(generics.GenericAPIView):
14 | """
15 | Channels publishers subscriptions
16 | """
17 | serializer_class = PublisherSerializer
18 |
19 | def post(self, request, format=None, name=None):
20 | pass
--------------------------------------------------------------------------------
/pushetta/api/pushes_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le funzionalità di push e lettura dei messaggi
5 |
6 | from datetime import datetime
7 | from dateutil.relativedelta import relativedelta
8 |
9 | from rest_framework import generics, permissions
10 | from rest_framework.response import Response
11 | from rest_framework import status
12 | from rest_framework.throttling import UserRateThrottle
13 |
14 | from django.conf import settings
15 |
16 | from core.models import Channel, ChannelMsg
17 | from api.serializers import PushMessageSerializer, PushResponseSerializer, TargetSerializer
18 |
19 | from core.services import send_push_message, get_push_targets, SendPushResponse
20 |
21 | class PushList(generics.GenericAPIView):
22 | serializer_class = PushMessageSerializer
23 | throttle_classes = (UserRateThrottle,)
24 |
25 | #permission_classes = (IsChannelOwner, permissions.IsAuthenticated,)
26 |
27 | def post(self, request, format=None, name=None):
28 | channel = None
29 | try:
30 | channel = Channel.objects.get(name=name)
31 | except Channel.DoesNotExist:
32 | return Response(status=status.HTTP_404_NOT_FOUND)
33 |
34 | if channel.owner != request.user:
35 | return Response(status=status.HTTP_403_FORBIDDEN)
36 |
37 | serializer = PushMessageSerializer(data=request.DATA)
38 | if not serializer.is_valid():
39 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
40 |
41 |
42 | push_msg = serializer.object
43 |
44 | expire = datetime.today() + relativedelta(months=1) # Default 1 mese
45 | if 'expire' in push_msg:
46 | expire = push_msg["expire"]
47 |
48 | target = None
49 | if "target" in push_msg:
50 | target = push_msg["target"]
51 |
52 | response = send_push_message(channel, push_msg["message_type"], push_msg["body"], expire, target)
53 |
54 | serializer = PushResponseSerializer(response)
55 | return Response(serializer.data, status=status.HTTP_200_OK)
56 |
57 | class TargetList(generics.GenericAPIView):
58 | serializer_class = TargetSerializer
59 |
60 | def get(self, request):
61 | resp_list = map(lambda x: { "target" : x}, get_push_targets())
62 | serializer = TargetSerializer(resp_list)
63 | return Response(serializer.source, status=status.HTTP_200_OK)
--------------------------------------------------------------------------------
/pushetta/api/serializers.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Serializer dei DTO
5 |
6 | from drf_compound_fields.fields import ListField
7 |
8 | from rest_framework import serializers
9 | from rest_framework.pagination import PaginationSerializer
10 | from rest_framework.serializers import ValidationError
11 | from django.contrib.auth.models import User
12 |
13 | from core.models import Channel, ChannelMsg, Subscriber, ChannelSubscribeRequest
14 |
15 |
16 | class UserSerializer(serializers.ModelSerializer):
17 | class Meta:
18 | model = User
19 | fields = ('id', 'username', 'first_name', 'last_name', 'email', )
20 |
21 |
22 | class ChannelSerializer(serializers.ModelSerializer):
23 | class Meta:
24 | model = Channel
25 | fields = ('id', 'name', 'image', 'description', 'kind', )
26 |
27 |
28 | class MinimalChannelSerializer(serializers.ModelSerializer):
29 | class Meta:
30 | model = Channel
31 | fields = ('id', 'name', 'image', )
32 |
33 |
34 | class PaginatedChannelSerializer(PaginationSerializer):
35 | start_index = serializers.SerializerMethodField('get_start_index')
36 | end_index = serializers.SerializerMethodField('get_end_index')
37 | num_pages = serializers.Field(source='paginator.num_pages')
38 |
39 | class Meta:
40 | object_serializer_class = ChannelSerializer
41 |
42 | def get_start_index(self, page):
43 | return page.start_index()
44 |
45 | def get_end_index(self, page):
46 | return page.end_index()
47 |
48 | def get_curr_page(self, page):
49 | return page.number
50 |
51 |
52 | class ChannelMsgSerializer(serializers.ModelSerializer):
53 | channel = MinimalChannelSerializer()
54 |
55 | class Meta:
56 | model = ChannelMsg
57 | fields = ('id', 'body', 'date_created', 'expire', 'channel', 'preview_url', )
58 |
59 |
60 | class SubscriberModelSerializer(serializers.ModelSerializer):
61 | class Meta:
62 | model = Subscriber
63 | fields = ('device_id', 'token', 'sub_type', 'name', )
64 |
65 |
66 | class SubscriberSerializer(serializers.Serializer):
67 | sub_type = serializers.CharField(max_length=100) # iOS, Android, ...
68 | device_id = serializers.CharField(max_length=200)
69 | token = serializers.CharField(max_length=500)
70 | name = serializers.CharField(max_length=250)
71 |
72 |
73 | class ChannelSubscriptionSerializer(serializers.Serializer):
74 | sub_type = serializers.CharField(max_length=100) # iOS, Android, ...
75 | device_id = serializers.CharField(max_length=200)
76 | token = serializers.CharField(max_length=500)
77 |
78 |
79 | class PushMessageSerializer(serializers.Serializer):
80 | message_type = serializers.CharField(max_length=100)
81 | body = serializers.CharField(max_length=500)
82 | expire = serializers.DateField(required=False)
83 | target = serializers.CharField(max_length=20, required=False)
84 |
85 |
86 | class PublisherSerializer(serializers.Serializer):
87 | dummy = serializers.CharField(max_length=100)
88 |
89 |
90 | class FeedbackSerializer(serializers.Serializer):
91 | device_id = serializers.CharField(max_length=200)
92 | messages_id = ListField(serializers.IntegerField())
93 |
94 |
95 | class PushResponseSerializer(serializers.Serializer):
96 | success = serializers.BooleanField()
97 | error_code = serializers.CharField(max_length=200)
98 |
99 |
100 | class CheckVersionSerializer(serializers.Serializer):
101 | need_update = serializers.BooleanField()
102 | message = serializers.CharField(max_length=200)
103 |
104 |
105 | class ChannelSubscribeRequestSerializer(serializers.ModelSerializer):
106 | channel = ChannelSerializer()
107 |
108 | class Meta:
109 | model = ChannelSubscribeRequest
110 | fields = ('channel', 'status')
111 |
112 |
113 | class TargetSerializer(serializers.WritableField):
114 |
115 | def from_native(self, data):
116 | if isinstance(data, list):
117 | return data
118 | else:
119 | msg = self.error_messages['invalid']
120 | raise ValidationError(msg)
121 |
122 | def to_native(self, obj):
123 | return obj
--------------------------------------------------------------------------------
/pushetta/api/subscriber_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le funzionalità per la gestione Subscribers
5 |
6 | from rest_framework import generics, permissions
7 | from rest_framework.response import Response
8 | from rest_framework import status
9 | from django.conf import settings
10 |
11 | from core.models import Subscriber, Channel, ChannelSubscribeRequest
12 | from core.models import ACCEPTED, PENDING
13 |
14 | from api.serializers import SubscriberSerializer, ChannelSerializer, ChannelSubscribeRequestSerializer
15 | from core.subscriber_manager import SubscriberManager
16 |
17 |
18 | class SubscriberList(generics.GenericAPIView):
19 | """
20 | Handle device subscription to Pushetta
21 | """
22 |
23 | serializer_class = SubscriberSerializer
24 |
25 | def post(self, request, format=None):
26 |
27 | serializer = SubscriberSerializer(data=request.DATA)
28 | if serializer.is_valid():
29 |
30 | is_sandbox = (True if settings.ENVIRONMENT == "dev" else False)
31 | subscriber_data = serializer.object
32 |
33 | subscriber, created = Subscriber.objects.get_or_create(device_id=subscriber_data["device_id"],
34 | defaults={'sub_type': subscriber_data["sub_type"],
35 | 'sandbox': is_sandbox, 'enabled': True,
36 | 'name': subscriber_data["name"],
37 | 'token': subscriber_data["token"]})
38 |
39 | if not created:
40 | subscriber.token = subscriber_data["token"]
41 | subscriber.name = subscriber_data["name"]
42 |
43 | subscriber.save()
44 |
45 | # Update del token nelle subscription del device
46 | subMamager = SubscriberManager()
47 | channel_subscriptions = subMamager.get_device_subscriptions(subscriber_data["device_id"])
48 | for channel_sub in channel_subscriptions:
49 | subMamager.subscribe(channel_sub, subscriber_data["sub_type"], subscriber_data["device_id"],
50 | subscriber_data["token"])
51 |
52 | return Response(serializer.data, status=status.HTTP_201_CREATED)
53 | else:
54 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
55 |
56 |
57 | class SubcriptionsList(generics.GenericAPIView):
58 | """
59 | Handle subscriptions to channels of a specific device
60 | """
61 |
62 | permission_classes = [
63 | permissions.AllowAny
64 | ]
65 |
66 | serializer_class = ChannelSerializer
67 |
68 | def get(self, request, format=None, deviceId=None):
69 | channel_names = SubscriberManager().get_device_subscriptions(deviceId)
70 |
71 | channels = Channel.objects.filter(name__in=channel_names)
72 | serializer = ChannelSerializer(channels, many=True)
73 |
74 | return Response(serializer.data)
75 |
76 |
77 | class DeviceSubscriptionsRequests(generics.GenericAPIView):
78 | """
79 | Handle list of device requests (subscribed and pending subscriptions)
80 | """
81 | permission_classes = [
82 | permissions.AllowAny
83 | ]
84 |
85 | serializer_class = ChannelSubscribeRequestSerializer
86 |
87 | def get(self, request, format=None, deviceId=None):
88 | channel_names = SubscriberManager().get_device_subscriptions(deviceId)
89 |
90 | # Uso ChannelSubscribeRequestSerializer e quelli già sottoscritti li aggiungo fake come ACCEPTED
91 | channels = Channel.objects.filter(name__in=channel_names)
92 | subscribed = [ChannelSubscribeRequest(channel=ch, device_id=deviceId, status=ACCEPTED) for ch in channels]
93 | # Le richieste visualizzate client side sono solo quelle
94 | requests = ChannelSubscribeRequest.objects.filter(device_id=deviceId).filter(status=PENDING)
95 |
96 | serializer = ChannelSubscribeRequestSerializer(subscribed + list(requests), many=True)
97 |
98 | return Response(serializer.data)
99 |
--------------------------------------------------------------------------------
/pushetta/api/sys_sl.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Service layer con le API generiche di sistema (tipo check versione client)
5 |
6 | from datetime import datetime
7 | from dateutil.relativedelta import relativedelta
8 |
9 | from rest_framework import generics, permissions
10 | from rest_framework.response import Response
11 | from rest_framework import status
12 | from django.conf import settings
13 |
14 | from api.permissions import IsChannelOwner
15 |
16 | from api.serializers import CheckVersionSerializer
17 |
18 | class CheckVersionData(object):
19 | def __init__(self, need_update, message):
20 | self.need_update = need_update
21 | self.message = message
22 |
23 |
24 |
25 | class CheckVersion(generics.GenericAPIView):
26 | serializer_class = CheckVersionSerializer
27 |
28 | def get(self, request, *args, **kwargs):
29 | checkData = CheckVersionData(False, "")
30 | serializer = CheckVersionSerializer(checkData)
31 | return Response(serializer.data, status=status.HTTP_200_OK)
32 |
--------------------------------------------------------------------------------
/pushetta/api/urls.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Definizione delle Url per la API
5 |
6 | from django.conf.urls import url, include
7 | from django.contrib.auth.decorators import login_required
8 |
9 | from api.channels_sl import ChannelsList, ChannelSearch, ChannelSubscription, ChannelSuggestion, ChannelUnSubscription
10 | from api.publisher_sl import PublisherList
11 | from api.pushes_sl import PushList, TargetList
12 | from api.subscriber_sl import SubscriberList, SubcriptionsList, DeviceSubscriptionsRequests
13 | from api.messages_sl import MessageList
14 | from api.feedback_sl import FeedbackService
15 | from api.crashes_sl import CrashLogService
16 | from api.sys_sl import CheckVersion
17 |
18 |
19 | channel_urls = [
20 | url(r'^/subscription/(?P
[\w|\W]+)/(?P[\w|\W]+)/(?P[\w|\W]+)/$', ChannelUnSubscription.as_view(), name='unsubscribe-channel-by-name'),
21 | url(r'^/subscription/(?P[\w|\W]+)/$', ChannelSubscription.as_view(),name='subscribe-channel-by-name'),
22 | url(r'^/$', ChannelsList.as_view(), name='channel-list'),
23 | url(r'^/search/$', ChannelSearch.as_view(), name='channel-search'),
24 | url(r'^/suggestions/(?P[\w|\W]+)/$', ChannelSuggestion.as_view(), name='channel-suggestions')
25 | ]
26 |
27 | publisher_urls = [
28 | url(r'^/channels/(?P[\w|\W]+)/$', PublisherList.as_view(), name='publishers-by-name'),
29 | ]
30 |
31 | messages_urls = [
32 | url(r'^/my/(?P[\w|\W]+)/$', MessageList.as_view(), name='messages-by-owner'),
33 | url(r'^/(?P[\w|\W]+)/$', MessageList.as_view(), name='messages-by-id'),
34 | ]
35 |
36 | pushes_urls = [
37 | url(r'^/(?P[\w|\W]+)/$', PushList.as_view(), name='pushes-by-name'),
38 | ]
39 |
40 | targets_urls = [
41 | url(r'^/$', TargetList.as_view(), name='target-list'),
42 | ]
43 |
44 | subscribers_urls = [
45 | url(r'^/$', SubscriberList.as_view(), name='subscribers-list'),
46 | ]
47 |
48 | subscriptions_urls = [
49 | url(r'^/requests/(?P[\w|\W]+)/$', DeviceSubscriptionsRequests.as_view(), name='requests-list'),
50 | url(r'^/(?P[\w|\W]+)/$', SubcriptionsList.as_view(), name='subsctiptions-list'),
51 | ]
52 |
53 | feedback_urls = [
54 |
55 | url(r'^/(?P[\w|\W]+)/$', FeedbackService.as_view(), name='feedback-one-message'),
56 | url(r'^/$', FeedbackService.as_view(), name='feedback-many-messages'),
57 | ]
58 |
59 | android_urls = [
60 | url(r'^/crashlog/', CrashLogService.as_view(), name='crashlog'),
61 | ]
62 |
63 | sys_urls = [
64 | url(r'^/version/$', CheckVersion.as_view(), name='sys-app-version'),
65 | ]
66 |
67 |
68 |
69 | urlpatterns = [
70 | # Autenticazione con AuthToken del django rest framework
71 | url(r'^auth/', 'rest_framework.authtoken.views.obtain_auth_token', name="auth-token"),
72 |
73 | url(r'^sys', include(sys_urls, namespace="sys-api")),
74 |
75 | url(r'^channels', include(channel_urls, namespace="channels-api")),
76 | url(r'^publisher', include(publisher_urls, namespace="publisher-api")),
77 | url(r'^pushes', include(pushes_urls, namespace="push-api")),
78 | url(r'^targets', include(targets_urls, namespace="target-api")),
79 |
80 | # NOTA :Verificare il porting del get di messages su questo
81 | url(r'^messages', include(messages_urls, namespace="messages-api")),
82 | url(r'^subscribers', include(subscribers_urls, namespace="subscribers-api")),
83 | url(r'^subscriptions', include(subscriptions_urls, namespace="subscriptions-api")),
84 | url(r'^feedback', include(feedback_urls, namespace="feedback-api")),
85 |
86 | url(r'^android/crashlog/', include(android_urls, namespace="android-api")),
87 | ]
--------------------------------------------------------------------------------
/pushetta/api/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.shortcuts import render
5 |
6 | # Create your views here.
7 |
--------------------------------------------------------------------------------
/pushetta/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/core/__init__.py
--------------------------------------------------------------------------------
/pushetta/core/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Progetto: Pushetta API
3 | # Estensioni della Admin
4 |
5 | from __future__ import unicode_literals
6 |
7 | from django.contrib import admin
8 |
9 | from models import Channel, ChannelMsg, Subscriber, StoredImage
10 |
11 | class ChannelAdmin(admin.ModelAdmin):
12 | list_display = ('name', 'description', 'subscriptions', 'date_created',)
13 |
14 | class ChannelMsgAdmin(admin.ModelAdmin):
15 | list_display = ('date_created', )
16 |
17 | class SubscriberAdmin(admin.ModelAdmin):
18 | list_display = ('sub_type', 'device_id', 'token', 'enabled', 'name', )
19 |
20 | admin.site.register(Channel, ChannelAdmin)
21 | admin.site.register(ChannelMsg, ChannelMsgAdmin)
22 | admin.site.register(Subscriber, SubscriberAdmin)
23 |
24 |
--------------------------------------------------------------------------------
/pushetta/core/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class CoreConfig(AppConfig):
8 | name = 'core'
9 |
--------------------------------------------------------------------------------
/pushetta/core/feedback_manager.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Manager for handling read feedback of channels messages
5 |
6 | import redis
7 | from django.conf import settings
8 |
9 |
10 | class FeedbackManager():
11 | """
12 | This class mantain an Set for every message containing all devices
13 | who reads the message
14 | """
15 |
16 |
17 | def __init__(self):
18 | pool = redis.ConnectionPool(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB)
19 | self.redis_client = redis.Redis(connection_pool=pool)
20 |
21 | def setFeedback(self, channel_name, message_id, device_id):
22 | return self.redis_client.sadd(self.__getSetKey(channel_name, message_id), device_id)
23 |
24 | def getFeedbackCount(self, channel_name, message_id):
25 | return self.redis_client.scard(self.__getSetKey(channel_name, message_id))
26 |
27 |
28 | def __getSetKey(self, channel_name, message_id):
29 | return "{0}:{1}:{2}:{3}".format(settings.REDIS_KEY_PREFIX, "fback", channel_name, message_id)
30 |
--------------------------------------------------------------------------------
/pushetta/core/fileds_validators.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Definizione dei validators custom per i model fileds
5 |
6 | from django.core.validators import RegexValidator
7 |
8 | # '^[\w]*$'
9 | isalphavalidator = RegexValidator(r'[a-zA-Z0-9_\-\s].*',
10 | message='invalid characters in field',
11 | code='Invalid value')
--------------------------------------------------------------------------------
/pushetta/core/push_manager.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Classes for interact with push server (Google Cloud Messaging, Apple Push Notification Server, ...)
5 |
6 | from push_providers.iOSPushProvider import iOSPushProvider
7 | from push_providers.AndroidPushProvider import AndroidPushProvider
8 | from push_providers.WP8PushProvider import WP8PushProvider
9 | from push_providers.TestPushProvider import TestPushProvider
10 | from push_providers.SafariPushProvider import SafariPushProvider
11 | from push_providers.MQTTPushProvider import MQTTPushProvider
12 |
13 | """
14 | Known push providers
15 | """
16 | providers_map = {
17 | 'ios': iOSPushProvider(),
18 | 'android': AndroidPushProvider(),
19 | 'wp8': WP8PushProvider(),
20 | 'test': TestPushProvider(),
21 | 'safari': SafariPushProvider(),
22 | 'chrome': AndroidPushProvider(),
23 | 'iot_device': MQTTPushProvider(),
24 | }
25 |
26 |
27 | class PushProviderFactory:
28 | """
29 | Factory class for different push providers
30 | """
31 |
32 | @staticmethod
33 | def create(providerType, logger):
34 | if not providerType in providers_map:
35 | assert 1, "Bad provider creation: " + providerType
36 | return None
37 | else:
38 | pusher = providers_map[providerType]
39 | pusher.logger = logger
40 | return pusher
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/pushetta/core/push_providers/AndroidPushProvider.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Provider for push to Google Cloud Messaging
5 |
6 | import logging
7 |
8 | import sys
9 | import traceback
10 | import random
11 | from pyfcm import FCMNotification
12 | from django.conf import settings
13 |
14 | from common import BaseProvider
15 |
16 | # Samples for FCM pip module here https://github.com/olucurious/pyfcm
17 |
18 | class AndroidPushProvider(BaseProvider):
19 | def __init__(self):
20 | self.gcm = FCMNotification(api_key=settings.FCM_SERVER_KEY)
21 |
22 |
23 | def pushMessage(self, message, destToken, channel_name):
24 | send_count = len(destToken)
25 |
26 | if send_count > 0:
27 | # TODO: Gestire paginazione dei tokens a gruppi di 6/700 (il limite dovrebbe essere 1000 per call)
28 | # TODO: gestione del ttl
29 | # Nota: converto in un dict perché altrimenti il serializzatore non riesce a lavorare sul PushMessage
30 | dic_obj = {'alert_msg': message.alert_msg, 'data_dic': message.data_dic, 'push_type': message.push_type}
31 | response = self.gcm.notify_multiple_devices(registration_ids=destToken, message_body=message.alert_msg, data_message=dic_obj)
32 |
33 | self.log_debug(str(response))
34 |
35 | # Handling errors
36 | if 'errors' in response:
37 | for error, reg_ids in response['errors'].items():
38 | self.log_error("push error {0}".format(error))
39 | # Check for errors and act accordingly
40 | if error is 'NotRegistered':
41 | pass
42 | # Remove reg_ids from database
43 | # for reg_id in reg_ids:
44 | # entity.filter(registration_id=reg_id).delete()
45 |
46 | if 'canonical' in response:
47 | for reg_id, canonical_id in response['canonical'].items():
48 | self.log_error("canonical {0}".format(reg_id))
49 | # # Repace reg_id with canonical_id in your database
50 | # entry = entity.filter(registration_id=reg_id)
51 | # entry.registration_id = canonical_id
52 | # entry.save()
53 |
54 | # Nota: al momento non viene gestito correttamente il conteggio dei push inviati, si da per scontato che tutti
55 | # lo siano
56 | else:
57 | self.log_info("Nothing to send for AndroidPushProvider")
58 |
59 |
60 | return send_count
--------------------------------------------------------------------------------
/pushetta/core/push_providers/MQTTPushProvider.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Push provider for MQTT protocol
5 |
6 | import sys
7 | import traceback
8 | import random
9 | from common import PushMessage, BaseProvider, PushProviderException
10 | import paho.mqtt.publish as publish
11 | from django.conf import settings
12 |
13 | class MQTTPushProvider(BaseProvider):
14 | push_pattern = "/pushetta.com/channels/{0}"
15 |
16 | def pushMessage(self, message, tokens, channel_name):
17 | if not isinstance(tokens, list):
18 | raise PushProviderException("tokens must be a list")
19 | return False
20 |
21 | if not isinstance(message, PushMessage):
22 | raise PushProviderException("message must be a PushMessage")
23 | return False
24 |
25 | # TODO: Condizionare il push verso MQTT al message.push_type ??
26 |
27 | try:
28 | topic = self.push_pattern.format(channel_name)
29 | auth_object = {'username': settings.MOSQ_USERNAME, 'password': settings.MOSQ_PASSWORD}
30 | publish.single(topic, payload=message.alert_msg, qos=1, retain=False, hostname=settings.MOSQ_HOST,
31 | port=settings.MOSQ_PORT, client_id="pushetta-api", keepalive=60, auth=auth_object)
32 | except:
33 | exc_type, exc_value, exc_traceback = sys.exc_info()
34 | lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
35 | self.log_error("MQTTPushProvider -- exception {0}".format(''.join('!! ' + line for line in lines)))
36 |
37 | return True
--------------------------------------------------------------------------------
/pushetta/core/push_providers/SafariPushProvider.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # push provider for Safari browser push notification
5 |
6 | import logging
7 | import sys
8 | import traceback
9 | import random
10 |
11 | from django.conf import settings
12 | import time
13 |
14 | from common import BaseProvider
15 | from apns import APNs, Frame, Payload
16 | from common import PushMessage, BaseProvider, PushProviderException
17 |
18 | # Procedura di invio massivo ispirata a https://gist.github.com/jimhorng/594401f68ce48282ced5
19 | SEND_INTERNAL = 0.01
20 |
21 | # Custom payload class fr Safari
22 |
23 |
24 | class PayloadSafari(Payload):
25 |
26 | def __init__(self, alert=None, url_args=[]):
27 | super(Payload, self).__init__()
28 | self.alert = alert
29 | self.url_args = url_args
30 | self._check_size()
31 |
32 | def dict(self):
33 | d = {"alert": self.alert, "url-args": self.url_args}
34 | d = {'aps': d}
35 | #print d
36 | return d
37 |
38 |
39 | class SafariPushProvider(BaseProvider):
40 |
41 | def response_listener(self, error_response):
42 | self.log_info(
43 | "SafariProvider:client get error-response: " + str(error_response))
44 |
45 | def wait_till_error_response_unchanged(self):
46 |
47 | if hasattr(self.apns.gateway_server, '_is_resending'):
48 | delay = 1
49 | count = 0
50 | while True:
51 | if self.apns.gateway_server._is_resending == False:
52 | time.sleep(delay)
53 | if self.apns.gateway_server._is_resending == False:
54 | count = count + delay
55 | else:
56 | count = 0
57 | else:
58 | count = 0
59 |
60 | if count >= 10:
61 | break
62 | return delay * count
63 | return 0
64 |
65 | def pushMessage(self, message, destToken, channel_name):
66 | send_count = len(destToken)
67 | if send_count > 0:
68 | try:
69 | # USO l'apns enanched
70 | self.apns = APNs(use_sandbox=settings.APNS_IS_SANDBOX,
71 | cert_file=settings.APNS_SAFARI_CERT_FILE, enhanced=True)
72 |
73 | alert_dict = {
74 | "title": message.alert_msg[:40], "body": message.alert_msg, "action": "View"}
75 | payload = PayloadSafari(alert=alert_dict, url_args=[
76 | message.data_dic['channel_name']])
77 |
78 | # TODO: Gestire l'expire
79 | # Expire in un mese di default (TODO: Ricavare dai dati passati)
80 | expiry = time.time() + 3600 * 24 * 30
81 |
82 | priority = 10
83 | for token in destToken:
84 | self.log_info(
85 | "SafariPushProvider -- send token {0}".format(token))
86 | identifier = random.getrandbits(32)
87 | self.apns.gateway_server.register_response_listener(
88 | self.response_listener)
89 | self.apns.gateway_server.send_notification(
90 | token, payload, identifier=identifier)
91 |
92 | # Get feedback messages.
93 | for (token_hex, fail_time) in self.apns.feedback_server.items():
94 | self.log_info(
95 | "SafariPushProvider -- feedback token {0} fail time {1}".format(token_hex, fail_time))
96 | delay = self.wait_till_error_response_unchanged()
97 | self.apns.gateway_server.force_close()
98 | except:
99 | exc_type, exc_value, exc_traceback = sys.exc_info()
100 | lines = traceback.format_exception(
101 | exc_type, exc_value, exc_traceback)
102 | self.log_error(
103 | "SafariPushProvider -- exception {0}".format(''.join('!! ' + line for line in lines)))
104 | else:
105 | self.log_info("Nothing to send for SafariPushProvider")
106 |
107 | return send_count
108 |
--------------------------------------------------------------------------------
/pushetta/core/push_providers/TestPushProvider.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Fake push provider for test only
5 |
6 | from common import PushMessage, BaseProvider, PushProviderException
7 |
8 |
9 | class TestPushProvider(BaseProvider):
10 | def pushMessage(self, message, tokens, channel_name):
11 | if not isinstance(tokens, list):
12 | raise PushProviderException("tokens must be a list")
13 | return False
14 |
15 | if not isinstance(message, PushMessage):
16 | raise PushProviderException("message must be a PushMessage")
17 | return False
18 |
19 | return True
--------------------------------------------------------------------------------
/pushetta/core/push_providers/WP8PushProvider.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Provider for push to Windows Phone 8 Platform
5 |
6 | import logging
7 |
8 | from mpns import MPNSToast
9 | from django.utils.http import urlquote
10 |
11 | from common import BaseProvider
12 |
13 |
14 | # Testare questa lib https://pypi.python.org/pypi/python-mpns/0.1.3
15 | # Eventualmente provare approccio low level http://stackoverflow.com/questions/19366308/python-windows-phone-8-authenticated-push
16 |
17 |
18 | class WP8PushProvider(BaseProvider):
19 |
20 | def pushMessage(self, message, destToken, channel_name):
21 | send_count = len(destToken)
22 | sent = 0
23 | if send_count > 0:
24 |
25 | toast = MPNSToast()
26 |
27 | # Nota il body per sicurezza va mandato solo in versione "short" per il tile
28 | # ma i dati del messaggio li prende l'app con una get sull'id
29 | paramString = "?body=" + urlquote(message.alert_msg) + "&push_type=" + message.push_type + "&"
30 |
31 | for k in message.data_dic:
32 | paramString += k + "=" + str(urlquote(message.data_dic[k])) + "&"
33 |
34 | for tok in destToken:
35 | toast.send(tok, {'text1': message.alert_msg, 'text2': 'Pushetta', 'param': paramString[:-1]})
36 | sent = sent + 1
37 | else:
38 | self.log_info("Nothing to send for WP8PushProvider")
39 |
40 |
41 | return sent
42 |
--------------------------------------------------------------------------------
/pushetta/core/push_providers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/core/push_providers/__init__.py
--------------------------------------------------------------------------------
/pushetta/core/push_providers/common.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Common for push providers
5 |
6 |
7 | class PushMessage:
8 | """
9 | A single message to send, aler_msg is show by device when it receive the notification.
10 | data_dic is the payolad of message
11 | """
12 |
13 | def __init__(self, alert_msg, push_type, data_dic):
14 | self.alert_msg = alert_msg
15 | self.push_type = push_type
16 | self.data_dic = data_dic
17 |
18 |
19 | class PushProviderException(Exception):
20 | pass
21 |
22 |
23 | # Base class for all providers
24 | # TODO: logging management is a crap!!! Please refactor
25 | class BaseProvider(object):
26 | logger = None
27 |
28 | def log_debug(self, message):
29 | if self.logger != None:
30 | self.logger.debug(message)
31 |
32 | def log_info(self, message):
33 | if self.logger != None:
34 | self.logger.info(message)
35 |
36 | def log_error(self, message):
37 | if self.logger != None:
38 | self.logger.error(message)
39 |
40 | def pushMessage(self, message, tokens, channel_name):
41 | raise NotImplementedError("Method must be implemented from subclass")
42 |
--------------------------------------------------------------------------------
/pushetta/core/push_providers/iOSPushProvider.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Provider for push to Apple Push Message System
5 |
6 | import logging
7 | import sys
8 | import traceback
9 | import random
10 |
11 | from django.conf import settings
12 | import time
13 |
14 | from common import BaseProvider
15 | from apns import APNs, Frame, Payload
16 |
17 |
18 | # Doc per implementazione qui https://github.com/djacobs/PyAPNs
19 | # Nota installare con "pip install git+git://github.com/djacobs/PyAPNs.git"
20 |
21 |
22 | # Procedura di invio massivo ispirata a https://gist.github.com/jimhorng/594401f68ce48282ced5
23 | SEND_INTERNAL = 0.01
24 |
25 |
26 | class iOSPushProvider(BaseProvider):
27 |
28 | def response_listener(self, error_response):
29 | self.log_info(
30 | "iOSprovider:client get error-response: " + str(error_response))
31 |
32 | def wait_till_error_response_unchanged(self):
33 |
34 | if hasattr(self.apns.gateway_server, '_is_resending'):
35 | delay = 1
36 | count = 0
37 | while True:
38 | if self.apns.gateway_server._is_resending == False:
39 | time.sleep(delay)
40 | if self.apns.gateway_server._is_resending == False:
41 | count = count + delay
42 | else:
43 | count = 0
44 | else:
45 | count = 0
46 |
47 | if count >= 10:
48 | break
49 | return delay * count
50 | return 0
51 |
52 | def pushMessage(self, message, destToken, channel_name):
53 | send_count = len(destToken)
54 | if send_count > 0:
55 | try:
56 |
57 | # USO l'apns enanched
58 | self.apns = APNs(use_sandbox=settings.APNS_IS_SANDBOX,
59 | cert_file=settings.APNS_CERT_FILE, enhanced=True)
60 |
61 | payload = Payload(
62 | alert=message.alert_msg, sound="notification.aiff", badge=1, custom=message.data_dic)
63 |
64 | # TODO: Gestire l'expire
65 | # Expire in un mese di default (TODO: Ricavare dai dati passati)
66 | expiry = time.time() + 3600 * 24 * 30
67 |
68 | priority = 10
69 | for token in destToken:
70 | self.log_info(
71 | "iOSPushProvider -- send token {0}".format(token))
72 | identifier = random.getrandbits(32)
73 | self.apns.gateway_server.register_response_listener(
74 | self.response_listener)
75 | self.apns.gateway_server.send_notification(
76 | token, payload, identifier=identifier)
77 |
78 | # Get feedback messages.
79 | for (token_hex, fail_time) in self.apns.feedback_server.items():
80 | self.log_info(
81 | "iOSPushProvider -- feedback token {0} fail time {1}".format(token_hex, fail_time))
82 | delay = self.wait_till_error_response_unchanged()
83 | self.apns.gateway_server.force_close()
84 | except:
85 | exc_type, exc_value, exc_traceback = sys.exc_info()
86 | lines = traceback.format_exception(
87 | exc_type, exc_value, exc_traceback)
88 | self.log_error(
89 | "iOSPushProvider -- exception {0}".format(''.join('!! ' + line for line in lines)))
90 | else:
91 | self.log_info("Nothing to send for iOSPushProvider")
92 |
93 | return send_count
94 |
--------------------------------------------------------------------------------
/pushetta/core/push_providers/test.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Test for push providers
5 |
6 | import logging
7 | logger = logging.getLogger(__name__)
8 |
9 | from datetime import datetime
10 | from datetime import timedelta
11 |
12 | from django.test import TestCase
13 | from django.conf import settings
14 |
15 | from core.push_manager import PushProviderFactory
16 | from core.push_providers.common import PushMessage, PushProviderException
17 |
18 | class PushProvidersTestCase(TestCase):
19 |
20 | """
21 | Test cases for Push Providers
22 | """
23 |
24 | def setUp(self):
25 | settings.ENVIRONMENT = "dev"
26 |
27 | def test_ios_provider(self):
28 | # Creazione del messaggio da "pushare"
29 | extra_data = {"message_id": 141, "channel_name": "Test", "channel_image_url": "http://www.pushetta.com/uploads/channel_media/05e704fc35e544dfa50bacf89dda0eee.jpeg",
30 | "ott": "111"}
31 |
32 | pmsg = PushMessage(alert_msg="Long text with ",
33 | push_type="plain_push",
34 | data_dic=extra_data)
35 |
36 | provider = PushProviderFactory.create('ios', logger)
37 | provider.pushMessage(pmsg, ['c0f5eb4db72a04d24d474e7cae5f1e7828f6f9bcaa8f083476e4840cc552a4c3'], "a channel")
38 |
39 | def test_mqtt_provider(self):
40 | extra_data = {
41 | "message_id": 141,
42 | "channel_name": "flower",
43 | "channel_image_url": "http://www.pushetta.com/uploads/channel_media/05e704fc35e544dfa50bacf89dda0eee.jpeg",
44 | "ott": "111"
45 | }
46 |
47 |
48 | pmsg = PushMessage(alert_msg="Message over MQTT test",
49 | push_type="plain_push",
50 | data_dic=extra_data)
51 |
52 | provider = PushProviderFactory.create('iot_device', logger)
53 | # Nota...il push su MQTT non richiede token perchè la subscription al canale è uno stato
54 | # persistente gestita dal protcollo (chi è connesso e ha fatto subscribe al topic riceve il messaggio)
55 | provider.pushMessage(pmsg, ['#'], "flower")
56 |
57 |
58 |
--------------------------------------------------------------------------------
/pushetta/core/search_indexes.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Indici per il motore di ricerca
5 |
6 | from haystack import indexes
7 |
8 | from core.models import Channel
9 |
10 | '''
11 | class ChannelIndex(indexes.ModelSearchIndex, indexes.Indexable):
12 | class Meta:
13 | model = Channel
14 |
15 | def index_queryset(self, using=None):
16 | """Used when the entire index for model is updated."""
17 | # filter(kind!=PRIVATE).
18 | return self.get_model().objects.filter(hidden=False)
19 | '''
20 |
21 |
22 | class ChannelIndex(indexes.SearchIndex, indexes.Indexable):
23 | text = indexes.EdgeNgramField(document=True, use_template=True)
24 |
25 | name = indexes.CharField(model_attr='name')
26 | description = indexes.CharField(model_attr='description')
27 | image = indexes.CharField(model_attr='image')
28 | hidden = indexes.BooleanField(model_attr='hidden')
29 | kind = indexes.IntegerField(model_attr='kind')
30 | subscriptions = indexes.IntegerField(model_attr='subscriptions')
31 |
32 | def get_model(self):
33 | return Channel
34 |
35 |
36 | def index_queryset(self, using=None):
37 | """Used when the entire index for model is updated."""
38 | # filter(kind!=PRIVATE).
39 | return self.get_model().objects.filter(hidden='false')
40 |
--------------------------------------------------------------------------------
/pushetta/core/security_helpers.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | import hmac
4 | import hashlib
5 | import base64
6 | import string
7 | import random
8 |
9 | import redis
10 | from django.conf import settings
11 |
12 |
13 | class OTTManager():
14 | """
15 | Handler of One Time Token
16 | """
17 |
18 | key_pattern = "{0}:ott:{1}"
19 |
20 | def __init__(self, ott_expire_seconds):
21 | pool = redis.ConnectionPool(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB)
22 | self.redis_client = redis.Redis(connection_pool=pool)
23 | self.ott_expire_seconds = ott_expire_seconds
24 |
25 | def getOneTimeToken(self):
26 | ott = self.__random_string_generator()
27 | # Il contenuto della chiave è insignificante, il token è rapresentato dall'esistenza della chiave stessa
28 | key = self.__get_key(ott)
29 | self.redis_client.set(key, 1)
30 | self.redis_client.expire(key, self.ott_expire_seconds)
31 | return ott
32 |
33 |
34 | def consumeOneTimeToken(self, ott):
35 | ret = False
36 | key = self.__get_key(ott)
37 | if self.redis_client.exists(key):
38 | self.redis_client.delete(key)
39 | ret = True
40 |
41 | return ret
42 |
43 | def existsOneTimeToken(self, ott):
44 | key = self.__get_key(ott)
45 | return self.redis_client.exists(key)
46 |
47 | def __random_string_generator(self, size=6, chars=string.ascii_uppercase + string.digits):
48 | return ''.join(random.choice(string.ascii_uppercase) for i in range(size))
49 | # return ''.join(random.choice(chars) for _ in range(size))
50 |
51 | def __get_key(self, ott):
52 | return self.key_pattern.format(settings.REDIS_KEY_PREFIX, ott)
53 |
54 |
55 | # Nota: seed è meglio che sia 20 caratteri
56 | def computeSignature(self, seed, contentType, dateString, methodPath):
57 | """
58 | Compute the signature to validate devices requeste
59 | seed -- seed shared with devices with push notifications
60 | contentType -- value of http header Content-Type
61 | dateString -- UTC string with request date dd-mm-yyyy format
62 | methodPath -- method of API request (absolute path)
63 | """
64 | # Nota: il seed è il seme condiviso tra client e server, se compromesso va cambiato
65 | # viene condiviso mediante notifiche push
66 | key = "4kGSVVETG8Ox/sF6zy6dFCpO97A3wUHr9jc41441"
67 | data = "POST\n\n{0}\n{1}\n{2}".format(contentType, dateString, methodPath)
68 |
69 | cKey = key.encode('ascii', 'ignore')
70 | cData = data.encode('ascii', 'ignore')
71 |
72 | dig = hmac.new(cKey, msg=cData, digestmod=hashlib.sha256).digest()
73 | b64HmacSha256 = base64.b64encode(dig).decode() # py3k-mode
74 | ret = "{1}:{0}".format(seed, b64HmacSha256)
75 | return ret
76 |
--------------------------------------------------------------------------------
/pushetta/core/signal.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta Core
4 | # Handling dei signals sui modelli
5 |
6 | import logging
7 | from datetime import datetime
8 | from dateutil.relativedelta import relativedelta
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | from django.dispatch import receiver
13 | from django.db.models.signals import pre_delete, pre_save
14 |
15 | from core.services import unsubscribe_channel
16 | from core.models import Channel, Subscriber, ChannelMsg
17 |
18 | from core.subscriber_manager import SubscriberManager
19 |
20 | '''
21 | Signal post delete del Channel che cancella i subscriber
22 | '''
23 |
24 |
25 | @receiver(signal=pre_delete, sender=Channel)
26 | def pre_remove_channel(sender, **kwargs):
27 | channel = kwargs.get('instance')
28 | sub_manager = SubscriberManager()
29 | sub_tokens = sub_manager.get_all_subscribers(channel.name)
30 | devices = Subscriber.objects.filter(token__in=sub_tokens)
31 |
32 | logger.info("Delete channel ({0}) signal ".format(channel.name))
33 |
34 | for dev in devices.all():
35 | logger.info("\t removing subscriber {0}".format(dev.device_id))
36 | unsubscribe_channel(channel, dev.device_id)
37 |
38 |
39 | '''
40 | Signal post delete del Subscriber che cancella le sue sottoscrizioni
41 | '''
42 |
43 |
44 | @receiver(signal=pre_delete, sender=Subscriber)
45 | def pre_remove_subscription(sender, **kwargs):
46 | subscriber = kwargs.get('instance')
47 | sub_manager = SubscriberManager()
48 | subscriptions = sub_manager.get_device_subscriptions(subscriber.device_id)
49 |
50 | logger.info("Delete subscriber ({0} - {1}) signal ".format(subscriber.name, subscriber.sub_type))
51 |
52 | for channel_name in subscriptions:
53 | unsubscribe_channel(channel, subscriber.device_id)
54 |
55 |
56 | '''
57 | Signal pre save del messaggio per bonificare l'expire se non passato
58 | '''
59 |
60 |
61 | @receiver(signal=pre_save, sender=ChannelMsg)
62 | def pre_default_expire(sender, **kwargs):
63 | channelMsg = kwargs.get('instance')
64 | if not channelMsg.expire:
65 | channelMsg.expire = (datetime.utcnow() + relativedelta(months=1))
--------------------------------------------------------------------------------
/pushetta/core/subscriber_manager.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Wrapper for interaction with Subscriber storage (Redis)
5 |
6 | # All strings in Unicode
7 | from __future__ import unicode_literals
8 |
9 | import redis
10 | from django.conf import settings
11 |
12 |
13 | class SubscriberManager():
14 | """
15 | This class mantain an Hash for every Channel containing all subscribers
16 | and a Set for every device containing all Channels subscribed
17 | """
18 |
19 | # Key part for hash key (subscriber to channel)
20 | subsc_key_part = "subsc"
21 | # Key part for set with devices subscribing a channel
22 | subdev_key_part = "subdev"
23 |
24 |
25 | def __init__(self):
26 | pool = redis.ConnectionPool(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB)
27 | self.redis_client = redis.Redis(connection_pool=pool)
28 |
29 |
30 | def flushdb(self):
31 | """
32 | WARNING: Remove all keys from current database
33 | """
34 | self.redis_client.flushdb()
35 |
36 | def subscribe(self, channel_name, sub_type, device_id, token):
37 | """
38 | Subscribe a device to a Channel
39 | """
40 |
41 | channel_name = channel_name.lower()
42 |
43 | key = self.__getHashKey(channel_name, sub_type)
44 |
45 | # Token in hash with Channel's subscribers
46 | self.redis_client.hset(key, self.__getFieldKey(device_id), token)
47 | skey = self.__getSetKey(device_id)
48 | # Channel name in set of channel subscribed by device
49 | self.redis_client.sadd(skey, channel_name)
50 |
51 | def unsubscribe(self, channel_name, device_id, sub_type):
52 | """
53 | Unsubscribe a device from a channel
54 | """
55 |
56 | channel_name = channel_name.lower()
57 |
58 | key = self.__getHashKey(channel_name, sub_type)
59 | # Token removed from hash with Channel's subscribers
60 | self.redis_client.hdel(key, self.__getFieldKey(device_id))
61 | # Channel name removed from set of channel subscribed by device
62 | skey = self.__getSetKey(device_id)
63 | self.redis_client.srem(skey, channel_name)
64 |
65 |
66 | def get_subscription(self, channel_name, sub_type, device_id):
67 | """
68 | Get channel subscription token for a device
69 | """
70 |
71 | channel_name = channel_name.lower()
72 |
73 | key = self.__getHashKey(channel_name, sub_type)
74 | field_value = self.redis_client.hget(key, self.__getFieldKey(device_id))
75 | return field_value
76 |
77 |
78 | def get_device_subscriptions(self, device_id):
79 | """
80 | Returns all Channel subscribed by a device
81 | """
82 | key = self.__getSetKey(device_id)
83 | return self.redis_client.smembers(key)
84 |
85 | def get_subscribers(self, channel_name, sub_type):
86 | """
87 | Return all tokens subscribing a channel
88 | """
89 |
90 | channel_name = channel_name.lower()
91 |
92 | key = self.__getHashKey(channel_name, sub_type)
93 | return self.redis_client.hvals(key)
94 |
95 | def get_all_subscribers(self, channel_name):
96 | """
97 | Return all tokens subscribing a channel
98 | """
99 |
100 | channel_name = channel_name.lower()
101 |
102 | keys = self.redis_client.keys(self.__getHashKeyPattern(channel_name))
103 | result = []
104 | for key in keys:
105 | for token in self.redis_client.hvals(key):
106 | result.append(token)
107 | return result
108 |
109 |
110 | def __getSetKey(self, device_id):
111 | return "{0}:{1}:{2}".format(settings.REDIS_KEY_PREFIX, self.subdev_key_part, device_id)
112 |
113 | def __getHashKeyPattern(self, channel_name):
114 | """
115 | Hash key composed by prefisx:subsc:channel_name:sub_type
116 | """
117 | return "{0}:{1}:{2}:*".format(settings.REDIS_KEY_PREFIX, self.subsc_key_part, channel_name)
118 |
119 |
120 | def __getHashKey(self, channel_name, sub_type):
121 | """
122 | Hash key composed by prefisx:subsc:channel_name:sub_type
123 | """
124 | return "{0}:{1}:{2}:{3}".format(settings.REDIS_KEY_PREFIX, self.subsc_key_part, channel_name, sub_type)
125 |
126 | def __getFieldKey(self, device_id):
127 | return device_id
--------------------------------------------------------------------------------
/pushetta/core/tasks.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Task asincroni gestiti da celery
5 |
6 |
7 | from celery.task.schedules import crontab
8 | from celery.task import task, periodic_task
9 | from celery.utils.log import get_task_logger
10 |
11 | from core.models import ChannelMsg
12 |
13 | from core.push_manager import PushProviderFactory
14 | from core.subscriber_manager import SubscriberManager
15 | from core.feedback_manager import FeedbackManager
16 | from core.utility import grab_url_screenshot
17 |
18 | # Per l'uso email standard Django
19 | from django.core.mail import EmailMessage
20 |
21 | from django.core.cache import cache
22 |
23 | logger = get_task_logger(__name__)
24 |
25 |
26 | @task(name='core.tasks.push_messages', ignore_result=False, max_retries=1)
27 | def push_messages(pmsg, channel_name, platform):
28 | """
29 |
30 | :param pmsg: Message to push
31 | :param channel_name: Channel to push
32 | :param platform: Target platform
33 | """
34 |
35 | sub_tokens = SubscriberManager().get_subscribers(channel_name, platform)
36 |
37 | logger.debug("Pushing {0} tokens to channel {1} for platform {2}".format(len(sub_tokens), channel_name, platform))
38 |
39 | # PUSH EFFETTIVO
40 | pusher = PushProviderFactory.create(platform, logger)
41 | pusher.pushMessage(pmsg, sub_tokens, channel_name)
42 |
43 |
44 | @task(name='core.tasks.read_feedback_multi', ignore_result=True, max_retries=1)
45 | def read_feedback_multi(deviceId, messageIds):
46 | """
47 |
48 | :param deviceId: Device id to mark read for
49 | :param messageIds: Array of id message to mark read
50 | """
51 | marked = 0
52 | try:
53 | for msgId in messageIds:
54 |
55 | msg = ChannelMsg.objects.get(id=msgId)
56 | c = msg.channel.name
57 |
58 | if FeedbackManager().setFeedback(c, msgId, deviceId) > 0:
59 | marked = marked + 1
60 |
61 | except ChannelMsg.DoesNotExist:
62 | logger.error("Trying to set read feedback on non existent message")
63 |
64 | return marked
65 |
66 |
67 | @periodic_task(name='core.tasks.search_reindex', run_every=crontab(hour=4, minute=30))
68 | def search_reindex():
69 | from haystack.management.commands import update_index
70 |
71 | update_index.Command().handle(using='default', remove=True)
72 | logger.info("Reindexing of search database")
73 |
74 |
75 | @task(name='sendMailMessage', ignore_result=True, max_retries=1)
76 | def sendMailMessage(subject, body, rcpts):
77 | msg = EmailMessage(subject, body, 'support@gumino.com', rcpts, headers={'Reply-To': 'no-reply@pushetta.com'})
78 | msg.send()
79 |
80 | LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes
81 |
82 |
83 | @task(name='get_screenshot', max_retries=3)
84 | def get_screenshot(url, message_id=None):
85 | # Questo task deve essere eseguito in modo seriale, solo uno in un dato
86 | # momento è attivo. Per questo si usa un lock
87 | ret = False
88 | lock_id = "screenshot_url-lock-id"
89 | acquire_lock = lambda: cache.add(lock_id, 'true', LOCK_EXPIRE)
90 | release_lock = lambda: cache.delete(lock_id)
91 |
92 | if acquire_lock():
93 | try:
94 | grabbed_url = grab_url_screenshot(url)
95 | if grabbed_url and message_id:
96 | message = ChannelMsg.objects.get(id=message_id)
97 | message.preview_url = grabbed_url
98 | message.save()
99 |
100 | ret = True
101 | except:
102 | logger.error("Try to run screenshot_url while already running")
103 | finally:
104 | release_lock()
105 |
106 | return ret
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/pushetta/core/utility.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta Core
4 | # Varie funzioni di utilità usate nel progetto
5 |
6 |
7 | import logging
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | import os
12 | import re
13 | import hashlib
14 |
15 | from urlparse import urlparse, urljoin
16 |
17 | from PIL import Image
18 | from ghost import Ghost
19 |
20 | from django.conf import settings
21 |
22 |
23 | def check_for_url(text):
24 | """
25 | Check if text contains one o more urls
26 | :param text: text where to look for the urls
27 | :return: array of found urls
28 | """
29 |
30 | #p = re.compile(ur'(?:https?:\/\/)?([\da-z\.-]+\.[a-z\.]{2,6}[\/\w]?)')
31 | p = re.compile(ur'(?:https?:\/\/)?([\da-z\.-]+\.[a-z\.]{2,6}[\/\w]?[]\w\/\.\-]*)')
32 | return re.findall(p, text)
33 |
34 | def grab_url_screenshot(url):
35 | """
36 | Grab an url making a screenshot of it
37 | Filename is SHA256 of url
38 | :param url:
39 | :return:
40 | """
41 |
42 | ret = None
43 |
44 | try:
45 | # Bonifica url (se manca lo schema assumo http://)
46 | url_res = urlparse(url)
47 | if not url_res.scheme:
48 | url = "http://" + url
49 |
50 | g = Ghost()
51 | with g.start() as ghost:
52 | page, res = ghost.open(url)
53 | if not page is None and page.http_status == 200:
54 | url_sha256 = hashlib.sha256(url).hexdigest()
55 | image_path = os.path.join('url_previews', url_sha256 + ".png")
56 | full_path = os.path.join(settings.MEDIA_ROOT, image_path)
57 |
58 | ghost.capture_to(full_path)
59 | ghost.page = None
60 |
61 | image_path = image_path.replace(".png", ".thumb.png")
62 | thumb_full_path = os.path.join(settings.MEDIA_ROOT,image_path)
63 | resize_and_crop(full_path, thumb_full_path, (550, 500))
64 | ret = urljoin(settings.BASE_URL, "uploads/" + image_path)
65 | else:
66 | logger.error("Failed to capture screenshot for {0}".format(url))
67 | except Exception, e:
68 | logger.exception(e)
69 | finally:
70 | del ghost
71 | return ret
72 |
73 |
74 | def resize_and_crop(img_path, modified_path, size, crop_type='top'):
75 | """
76 | Resize and crop an image to fit the specified size.
77 |
78 | args:
79 | img_path: path for the image to resize.
80 | modified_path: path to store the modified image.
81 | size: `(width, height)` tuple.
82 | crop_type: can be 'top', 'middle' or 'bottom', depending on this
83 | value, the image will cropped getting the 'top/left', 'middle' or
84 | 'bottom/right' of the image to fit the size.
85 | raises:
86 | Exception: if can not open the file in img_path of there is problems
87 | to save the image.
88 | ValueError: if an invalid `crop_type` is provided.
89 | """
90 | # If height is higher we resize vertically, if not we resize horizontally
91 | img = Image.open(img_path)
92 | # Get current and desired ratio for the images
93 | img_ratio = img.size[0] / float(img.size[1])
94 | ratio = size[0] / float(size[1])
95 | #The image is scaled/cropped vertically or horizontally depending on the ratio
96 | if ratio > img_ratio:
97 | img = img.resize((size[0], int(round(size[0] * img.size[1] / img.size[0]))),
98 | Image.ANTIALIAS)
99 | # Crop in the top, middle or bottom
100 | if crop_type == 'top':
101 | box = (0, 0, img.size[0], size[1])
102 | elif crop_type == 'middle':
103 | box = (0, int(round((img.size[1] - size[1]) / 2)), img.size[0],
104 | int(round((img.size[1] + size[1]) / 2)))
105 | elif crop_type == 'bottom':
106 | box = (0, img.size[1] - size[1], img.size[0], img.size[1])
107 | else :
108 | raise ValueError('ERROR: invalid value for crop_type')
109 | img = img.crop(box)
110 | elif ratio < img_ratio:
111 | img = img.resize((int(round(size[1] * img.size[0] / img.size[1])), size[1]),
112 | Image.ANTIALIAS)
113 | # Crop in the top, middle or bottom
114 | if crop_type == 'top':
115 | box = (0, 0, size[0], img.size[1])
116 | elif crop_type == 'middle':
117 | box = (int(round((img.size[0] - size[0]) / 2)), 0,
118 | int(round((img.size[0] + size[0]) / 2)), img.size[1])
119 | elif crop_type == 'bottom':
120 | box = (img.size[0] - size[0], 0, img.size[0], img.size[1])
121 | else :
122 | raise ValueError('ERROR: invalid value for crop_type')
123 | img = img.crop(box)
124 | else :
125 | img = img.resize((size[0], size[1]),
126 | Image.ANTIALIAS)
127 | # If the scale is the same, we do not need to crop
128 | img.save(modified_path)
129 |
--------------------------------------------------------------------------------
/pushetta/core/utils.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta Core
4 | # Varie funzioni di utilità usate nel progetto
5 |
6 | from django.utils import six
7 | from django.conf import settings
8 |
9 |
10 | def custom_get_identifier(obj_or_string):
11 | """
12 | Get an unique identifier for the object or a string representing the
13 | object.
14 |
15 | If not overridden, uses ...
16 | """
17 | if isinstance(obj_or_string, six.string_types):
18 | if not IDENTIFIER_REGEX.match(obj_or_string):
19 | raise AttributeError(u"Provided string '%s' is not a valid identifier." % obj_or_string)
20 |
21 | return obj_or_string
22 |
23 | return u"%s" % (obj_or_string._get_pk_val())
24 |
--------------------------------------------------------------------------------
/pushetta/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pushetta.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/pushetta/pushetta/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | # This will make sure the app is always imported when
5 | # Django starts so that shared_task will use this app.
6 | from .celery import app as celery_app
7 | from core import signal
8 |
9 | __version__ = '1.1.5'
10 | __build__ = ''
11 |
--------------------------------------------------------------------------------
/pushetta/pushetta/celery.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | import os
4 |
5 | from celery import Celery
6 | from django.conf import settings
7 |
8 | # set the default Django settings module for the 'celery' program.
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pushetta.settings')
10 |
11 | app = Celery('pushetta')
12 |
13 | # Using a string here means the worker will not have to
14 | # pickle the object when using Windows.
15 | app.config_from_object('django.conf:settings')
16 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
17 |
18 |
19 | @app.task(bind=True)
20 | def debug_task(self):
21 | print('Request: {0!r}'.format(self.request))
22 |
--------------------------------------------------------------------------------
/pushetta/pushetta/urls.py:
--------------------------------------------------------------------------------
1 | """pushetta URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import include, url
17 | from django.contrib import admin
18 |
19 | from django.conf import settings
20 |
21 | urlpatterns = [
22 | url(r'^admin/', admin.site.urls),
23 | url(r'^', include('www.urls')),
24 | url(r'^admin/', include(admin.site.urls)),
25 | url(r'^api/', include('api.urls')),
26 | ]
27 |
28 | if settings.SERVE_STATIC == True:
29 | urlpatterns.append(url(r'^static/(?P.*)$', 'django.views.static.serve', {'document_root': settings.STATIC_ROOT}))
30 | urlpatterns.append(url(r'^uploads/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}))
31 |
--------------------------------------------------------------------------------
/pushetta/pushetta/uwsgi.ini:
--------------------------------------------------------------------------------
1 | [uwsgi]
2 | http-socket = 0.0.0.0:8001
3 | chmod-socket = 666
4 | processes = 1
5 | master = true
6 | logto = %v/uwsgi.log
7 | uid = pushetta
8 | chdir = %v/pushetta/
9 | env = DJANGO_SETTINGS_MODULE=pushetta.settings
10 | module = pushetta.wsgi:application
11 |
--------------------------------------------------------------------------------
/pushetta/pushetta/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for pushetta project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pushetta.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/pushetta/www/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/www/__init__.py
--------------------------------------------------------------------------------
/pushetta/www/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class WwwConfig(AppConfig):
8 | name = 'www'
9 |
--------------------------------------------------------------------------------
/pushetta/www/browser_views.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta Site
4 | # Class view per la gestione dei metodi legati alla gestione delle push si Chrome
5 |
6 | import json
7 | import logging
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | from django.http import HttpResponse
12 | from django.views.generic import View
13 |
14 | from django.shortcuts import get_object_or_404
15 |
16 | from core.services import ask_subscribe_channel
17 |
18 | from core.models import Subscriber, Channel, User
19 | from core.subscriber_manager import SubscriberManager
20 |
21 |
22 | class WebPushRegistration(View):
23 | """
24 | Custom API to handle post of registration data (user, token,...)
25 | Invoked by Ajax call in callback of permissionRequest client side
26 | """
27 |
28 |
29 |
30 | # Check if device_id is subscriber of channel_name
31 | def get(self, request, device_id=None, channel_name=None):
32 | channel = get_object_or_404(Channel, name=channel_name)
33 |
34 | channels = SubscriberManager().get_device_subscriptions(device_id)
35 | resp = 200
36 | if next((x for x in channels if x == channel.name.lower()), None) == None:
37 | resp = 404
38 |
39 |
40 | return HttpResponse(status=resp)
41 |
42 | # Subscribe to a channel
43 | def post(self, request):
44 | post_data = json.loads(request.body)
45 | channel_name = None
46 | if 'channel' in post_data:
47 | channel_name = post_data['channel']
48 |
49 | deviceToken = post_data['token']
50 | browser = post_data['browser']
51 | deviceId = post_data['device_id']
52 |
53 | name = "-"
54 | if request.user.is_authenticated():
55 | name = request.user.username
56 |
57 | # Create il subscriber if it doesn't exist
58 | subscriber, created = Subscriber.objects.update_or_create(device_id=deviceId,
59 | defaults={'sub_type': browser,
60 | 'sandbox': False, 'enabled': True,
61 | 'name': name,
62 | 'token': deviceToken})
63 |
64 | # Channel subscription
65 | if channel_name is not None:
66 | channel = get_object_or_404(Channel, name=channel_name)
67 | ask_subscribe_channel(channel, deviceId)
68 |
69 | return HttpResponse(status=201 if created else 200)
70 |
71 | # Delete a channel subscription
72 | def delete(self, request, device_id=None, channel_name=None):
73 | channel = get_object_or_404(Channel, name=channel_name)
74 | channels = SubscriberManager().get_device_subscriptions(device_id)
75 |
76 | resp = 404
77 | if next((x for x in channels if x == channel.name.lower()), None) != None:
78 | current_dev = Subscriber.objects.get(device_id=device_id)
79 | SubscriberManager().unsubscribe(channel_name, device_id, current_dev.sub_type)
80 | resp = 200
81 |
82 | return HttpResponse(status=resp)
83 |
84 |
85 |
--------------------------------------------------------------------------------
/pushetta/www/dispatcher_view.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta Site
4 | # Calss view per la gestione del dispatcher su device mobile
5 |
6 | import logging
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | from django.http import HttpResponse, HttpResponseRedirect
11 | from django.views.generic import View
12 | from django.core.urlresolvers import reverse
13 |
14 | from django.shortcuts import render_to_response, get_object_or_404
15 |
16 |
17 | from django.template import RequestContext
18 | from core.models import Channel
19 |
20 | from user_agents import parse
21 |
22 |
23 | class ChannelSubscriberDispatcher(View):
24 | def get(self, request, channel_name=None):
25 | ua_string = request.META.get('HTTP_USER_AGENT', '')
26 | user_agent = parse(ua_string)
27 |
28 | logger.info(user_agent.browser.family)
29 |
30 | if user_agent.os.family == 'Android':
31 | return HttpResponseRedirect(reverse('android-subscribe', args=[channel_name]))
32 | elif user_agent.os.family == "iOS":
33 | return HttpResponseRedirect(reverse('ios-subscribe', args=[channel_name]))
34 |
35 | channel = get_object_or_404(Channel, name=channel_name)
36 | return render_to_response("channel-dispatcher.html", {
37 | 'channel': channel,
38 | 'browser' : user_agent.browser.family,
39 | },
40 | context_instance=RequestContext(request))
41 |
42 |
43 |
44 |
45 | # Identificazione del mobile device per reagire nel modo corretto con il dispatcher
46 | # Può tornare : iphone, ipad, android, blackberry, wp7, wp8 o wp (windows mobile generico)
47 | def __mobile(self, request):
48 |
49 | device = ''
50 |
51 | ua = request.META.get('HTTP_USER_AGENT', '').lower()
52 |
53 | if ua.find("iphone") > 0:
54 | device = "iphone" # + re.search("iphone os (\d)", ua).groups(0)[0]
55 |
56 | if ua.find("ipad") > 0:
57 | device = "ipad"
58 |
59 | if ua.find("android") > 0:
60 | device = "android" # + re.search("android (\d\.\d)", ua).groups(0)[0].translate(None, '.')
61 |
62 | if ua.find("blackberry") > 0:
63 | device = "blackberry"
64 |
65 | if ua.find("windows phone os 7") > 0:
66 | device = "wp7"
67 | elif ua.find("windows phone 8") > 0:
68 | device = "wp8"
69 | elif ua.find("iemobile") > 0:
70 | device = "wp"
71 |
72 | if not device: # either desktop, or something we don't care about.
73 | device = "baseline"
74 |
75 | return device
76 |
77 | class AndroidSubscribe(View):
78 |
79 | def get(self, request, channel_name=None):
80 | channel = None
81 | try:
82 | channel = Channel.objects.get(name=channel_name)
83 | except Channel.DoesNotExist:
84 | pass
85 |
86 | return render_to_response("android-subscribe.html", {'channel': channel},
87 | context_instance=RequestContext(request))
88 |
89 | class iOSSubscribe(View):
90 |
91 | def get(self, request, channel_name=None):
92 | channel = None
93 | try:
94 | channel = Channel.objects.get(name=channel_name)
95 | except Channel.DoesNotExist:
96 | pass
97 |
98 | return render_to_response("ios-subscribe.html", {'channel': channel},
99 | context_instance=RequestContext(request))
100 |
101 | class SafariSubscribe(View):
102 |
103 | def get(self, request, channel_name=None):
104 | channel = None
105 | try:
106 | channel = Channel.objects.get(name=channel_name)
107 | except Channel.DoesNotExist:
108 | pass
109 |
110 | return render_to_response("safari-subscribe.html", {'channel': channel},
111 | context_instance=RequestContext(request))
112 |
113 | class ChromeSubscribe(View):
114 |
115 | def get(self, request, channel_name=None):
116 | channel = None
117 | try:
118 | channel = Channel.objects.get(name=channel_name)
119 | except Channel.DoesNotExist:
120 | pass
121 |
122 | return render_to_response("chrome-subscribe.html", {'channel': channel},
123 | context_instance=RequestContext(request))
124 |
--------------------------------------------------------------------------------
/pushetta/www/forms.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Definizione delle form usate nel progetto
5 |
6 | from django.forms import ModelForm
7 | from django import forms
8 |
9 | from core.models import Channel, ChannelMsg
10 |
11 |
12 | class ChannelForm(ModelForm):
13 | class Meta:
14 | model = Channel
15 | fields = ['name', 'image', 'description', 'kind', 'hidden']
16 |
17 | widgets = {
18 | 'name': forms.TextInput(attrs={
19 | 'class': 'span6',
20 | 'placeholder': 'Choose a name for Your channel'
21 | }),
22 | 'description': forms.Textarea(attrs={
23 | 'class': 'span6',
24 | 'rows': 4,
25 | 'placeholder': 'Give a brief description of the meaning of Your channel'
26 | }),
27 | 'image': forms.HiddenInput()
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/pushetta/www/safari_views.py:
--------------------------------------------------------------------------------
1 | # Progetto: Pushetta Site
2 | # Class view per la gestione dei metodi legati alla gestione del token per le push su Safari
3 |
4 | # -*- coding: utf-8 -*-
5 | from __future__ import unicode_literals
6 |
7 | import os
8 | import json
9 | import logging
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 | from datetime import datetime
14 | from dateutil.relativedelta import relativedelta
15 |
16 | from django.conf import settings
17 |
18 | from django.http import Http404
19 | from django.http import HttpResponse, HttpResponseBadRequest
20 | from django.views.generic import View
21 | from django.views.generic.edit import DeletionMixin
22 | from django.views.generic.edit import CreateView
23 | from django.template import RequestContext
24 |
25 | from django.shortcuts import render_to_response, get_object_or_404
26 | from django.core.files import File
27 |
28 | from wsgiref.util import FileWrapper
29 |
30 | from core.subscriber_manager import SubscriberManager
31 | from core.services import ask_subscribe_channel, unsubscribe_channel, get_device_subscriptions
32 |
33 | from core.models import Subscriber, ChannelSubscribeRequest, Channel
34 | from core.models import ACCEPTED, REJECTED, PENDING, SUBSCRIBE_REQ_STATUS
35 |
36 |
37 |
38 | class SafariPushService(View):
39 | """
40 | Handle token registration for Safari push
41 | """
42 |
43 | def post(self, request, deviceToken=None, websitePushID=None):
44 | logger.info("POST deviceToken={0} websitePushID={1}".format(deviceToken, websitePushID))
45 | return HttpResponse(status=200)
46 |
47 | def delete(self, request, deviceToken=None, websitePushID=None):
48 | logger.info("DELETE deviceToken={0} auth={1}".format(deviceToken, request.META['HTTP_AUTHORIZATION']))
49 | # Rimozione della subscription (a tendere valutare logical delete)
50 | # Subscriber.objects.filter(token=deviceToken).delete()
51 | subscribed_channels = get_device_subscriptions(deviceToken)
52 |
53 | for channel in [c.channel for c in subscribed_channels]:
54 | unsubscribe_channel(channel, deviceToken)
55 |
56 | return HttpResponse(status=200)
57 |
58 |
59 | class SafariLogService(View):
60 | """
61 | Handle log of errors for Safari push
62 | """
63 |
64 | def post(self, request):
65 | logger.info("LOG BODY {0}".format(request.body))
66 | return HttpResponse(status=200)
67 |
68 |
69 | class SafariPackageDownloadService(View):
70 | """
71 | Handle download of Website Package for Safari push notification
72 | """
73 |
74 | def post(self, request, deviceToken=None):
75 | PACKAGE_FILE = os.path.join(settings.BASE_DIR, '..', "var/Pushetta.pushpackage.zip")
76 | packageFile = open(PACKAGE_FILE, 'rb')
77 |
78 | response = HttpResponse(FileWrapper(packageFile), content_type='application/zip')
79 | response['Content-Disposition'] = 'attachment; filename="%s"' % 'Pushetta.pushpackage.zip'
80 | return response
81 |
--------------------------------------------------------------------------------
/pushetta/www/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guglielmino/pushetta-api-django/f5612d64d1d1445f91ee3a8717d83b17be96de48/pushetta/www/templatetags/__init__.py
--------------------------------------------------------------------------------
/pushetta/www/templatetags/activemenu.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Custom tags
5 |
6 | from django import template
7 | from django.core import urlresolvers
8 |
9 |
10 | register = template.Library()
11 |
12 |
13 | @register.simple_tag(takes_context=True)
14 | def active(context, url_name, return_value=' active', **kwargs):
15 | matches = current_url_equals(context, url_name, **kwargs)
16 | return return_value if matches else ''
17 |
18 |
19 | def current_url_equals(context, url_name, **kwargs):
20 | resolved = False
21 | try:
22 | resolved = urlresolvers.resolve(context.get('request').path)
23 | except:
24 | pass
25 | matches = resolved and resolved.url_name == url_name
26 | if matches and kwargs:
27 | for key in kwargs:
28 | kwarg = kwargs.get(key)
29 | resolved_kwarg = resolved.kwargs.get(key)
30 | if not resolved_kwarg or kwarg != resolved_kwarg:
31 | return False
32 | return matches
--------------------------------------------------------------------------------
/pushetta/www/templatetags/storelink.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Progetto: Pushetta API
4 | # Custom tag con la definizione dei link per il download delle App dai vari store
5 |
6 | from django import template
7 |
8 | register = template.Library()
9 |
10 | store_links = {'android': "/apps",
11 | 'ios': "/apps",
12 | 'wp8': "/apps"}
13 |
14 |
15 | @register.simple_tag(takes_context=True)
16 | def storelink(context, platform, **kwargs):
17 | return_value = ""
18 | if platform in store_links:
19 | return_value = store_links[platform]
20 | return return_value
21 |
--------------------------------------------------------------------------------
/pushetta/www/upload_views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import os
5 |
6 | from PIL import Image
7 |
8 | from django.shortcuts import render_to_response, get_object_or_404
9 | from django.conf import settings
10 | from django.http import HttpResponse, HttpResponseBadRequest
11 | from core.models import StoredImage
12 | from django.core.files.uploadedfile import UploadedFile
13 |
14 |
15 | import json
16 |
17 | # Nota: a tendere va rimosso con la gestione client del CSRF
18 | from django.views.decorators.csrf import csrf_exempt
19 |
20 | import logging
21 |
22 | log = logging
23 |
24 |
25 | @csrf_exempt
26 | def multiuploader_delete(request, pk):
27 | """
28 | View for deleting photos with multiuploader AJAX plugin.
29 | made from api on:
30 | https://github.com/blueimp/jQuery-File-Upload
31 | """
32 | if request.method == 'POST':
33 | log.info('Called delete image. Photo id=' + str(pk))
34 | image = get_object_or_404(Image, pk=pk)
35 | image.delete()
36 | log.info('DONE. Deleted photo id=' + str(pk))
37 | return HttpResponse(str(pk))
38 | else:
39 | log.info('Recieved not POST request todelete image view')
40 | return HttpResponseBadRequest('Only POST accepted')
41 |
42 |
43 | @csrf_exempt
44 | def multiuploader(request):
45 | if request.method == 'POST':
46 | res = dict(success=False, error="", name="", url="")
47 |
48 | if request.FILES == None:
49 | return HttpResponseBadRequest('Must have files attached!')
50 |
51 | #getting file data for farther manipulations
52 | file = request.FILES[u'files[]']
53 | wrapped_file = UploadedFile(file)
54 | filename = wrapped_file.name
55 | file_size = wrapped_file.file.size
56 | log.info('Got file: "' + str(filename) + '"')
57 |
58 | try:
59 | image = Image.open(file)
60 | #To get the image size, in pixels.
61 | (width,height) = image.size
62 | if width != 256 and height != 256:
63 | res["success"] = False
64 | res["error"] = "Image size must be 256x256 px"
65 | else:
66 | log.info('image w:{0} h:{1}'.format(width,height))
67 |
68 | storedimg = StoredImage()
69 | storedimg.title = ""
70 | storedimg.image = file
71 | storedimg.save()
72 |
73 | #getting file url here
74 | file_url = settings.MEDIA_URL
75 |
76 | res["success"] = True
77 | res["name"] = str(storedimg)
78 | res["url"] = file_url
79 | res["error"] = ""
80 | except IOError:
81 | res["success"] = False
82 | res["error"] = "Format not recognized"
83 |
84 | response_data = json.dumps(res)
85 | return HttpResponse(response_data, content_type='application/json')
86 | else: #GET
87 | return render_to_response('multiuploader_main.html',
88 | dict(static_url=settings.MEDIA_URL, open_tv=u'{{', close_tv=u'}}'),
89 | )
90 |
91 |
92 | def image_view(request):
93 | items = StoredImage.objects.all()
94 | return render_to_response('images.html', {'items': items})
--------------------------------------------------------------------------------
/pushetta/www/urls.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | # Progetto: Pushetta API
5 | # Definizione delle Url per la API
6 |
7 | from django.conf.urls import url, include
8 | from django.views.generic import TemplateView
9 | from django.contrib.auth.decorators import login_required
10 |
11 | from dispatcher_view import ChannelSubscriberDispatcher, AndroidSubscribe, iOSSubscribe, SafariSubscribe, \
12 | ChromeSubscribe
13 | from my_views import MyDashboardView, MyChannelsView, ChannelCreate, ChannelSearchView, ApproveRequestsView, \
14 | UpdateChannelView
15 |
16 | from safari_views import SafariPushService, SafariPackageDownloadService, SafariLogService
17 | from browser_views import WebPushRegistration
18 |
19 | from upload_views import multiuploader, multiuploader_delete
20 |
21 | from django.views.decorators.csrf import csrf_exempt
22 |
23 | # Funzionalità disponibili solo all'utente loggato
24 | privatearea_urls = [
25 | url(r'^dashboard/$', login_required(MyDashboardView.as_view()), name="pushetta-dashboard"),
26 | url(r'^channels/$', login_required(MyChannelsView.as_view()), name="my-channels"),
27 | url(r'^channels/create/$', login_required(ChannelCreate.as_view()), name="my-channelcreate"),
28 | url(r'^channels/update-props/$', login_required(UpdateChannelView.as_view()), name="my-channelupdate-props"),
29 | url(r'^channels/(?P[\w|\W]+)/push/$', login_required(MyChannelsView.as_view()),
30 | name="my-channel-pushmessage"),
31 | url(r'^channels/(?P[\w|\W]+)/$', login_required(MyChannelsView.as_view()), name="my-channelview"),
32 | url(r'^requests/approve/$', login_required(ApproveRequestsView.as_view()), name="my-approve-requests"),
33 | ]
34 |
35 | # Url per la gestione della sottoscrizione ai Canali con gestione
36 | # del device di provenienza (Android, iOs, Wp, ...)
37 | channel_subscription_urls = [
38 | url(r'^dispatch/(?P[\w|\W]+)/$', ChannelSubscriberDispatcher.as_view(), name="channel-subscription-dispatcher"),
39 | url(r'^android/(?P[\w|\W]+)/$', AndroidSubscribe.as_view(), name="android-subscribe"),
40 | url(r'^ios/(?P[\w|\W]+)/$', iOSSubscribe.as_view(), name="ios-subscribe"),
41 | url(r'^safari/(?P[\w|\W]+)/$',login_required(SafariSubscribe.as_view()), name="safari-subscribe"),
42 | url(r'^chrome/(?P[\w|\W]+)/$', login_required(ChromeSubscribe.as_view()),name="chrome-subscribe"),
43 | ]
44 |
45 | file_uploads_urls = [
46 | url(r'^delete/(\d+)/$', multiuploader_delete, name="upload-delete"),
47 | url(r'^multi/$', multiuploader, name="upload-file"),
48 | ]
49 |
50 | # Url per la gestione della meccanica per il push to browser Safari
51 | safari_urls = [
52 | url(r'^/devices/(?P[\w|\W]+)/registrations/web.com.pushetta$', csrf_exempt(SafariPushService.as_view()), name='safari-token-register'),
53 | url(r'^/pushPackages/web.com.pushetta$', csrf_exempt(SafariPackageDownloadService.as_view()), name='safari-package-download'),
54 | url(r'^/log', csrf_exempt(SafariLogService.as_view()), name='safari-log'),
55 | ]
56 |
57 | browser_urls = [
58 | # Endpoint per la registrazione anonima del solo device
59 | url(r'^register/device$', csrf_exempt(WebPushRegistration.as_view()), name='browser-registration-device'),
60 | url(r'^register/(?P[\w|\W]+)/(?P[\w|\W]+)/$', login_required(csrf_exempt(WebPushRegistration.as_view())), name='browser-get-registration'),
61 | url(r'^register$', login_required(csrf_exempt(WebPushRegistration.as_view())), name='browser-registration'),
62 | ]
63 |
64 | urlpatterns = [
65 | url(r'^$', TemplateView.as_view(template_name="index.html"), name="pushetta-index"),
66 | url(r'^cookie_policy/$', TemplateView.as_view(template_name="cookie_policy.html"), name="cookie_policy"),
67 | url(r'^apps/$', TemplateView.as_view(template_name="apps.html"), name="pushetta-apps"),
68 | url(r'^search/$', ChannelSearchView.as_view(), name="site-channel-search"),
69 | url(r'^my/', include(privatearea_urls)),
70 | url(r'^pushetta-search/$', ChannelSearchView.as_view(), name="pushetta-search"),
71 | url(r'^pushetta-api/', TemplateView.as_view(template_name="api-docs.html"), name="pushetta-api-doc"),
72 | url(r'^pushetta-docs/$', TemplateView.as_view(template_name="docs.html"), name="pushetta-docs"),
73 | url(r'^pushetta-downloads/$', TemplateView.as_view(template_name="downloads.html"), name="pushetta-downloads"),
74 |
75 | # API per l'integrazione push Safari
76 | url(r'^safari/v1', include(safari_urls)),
77 |
78 | # API per la gestione dei token acquisiti dal browser per le notifiche Push
79 | url(r'^browser/', include(browser_urls)),
80 |
81 | # Login, registrazione, ... con allauth
82 | url(r'^accounts/', include('allauth.urls')),
83 |
84 | url(r'subs/', include(channel_subscription_urls)),
85 | url(r'resource/', include(file_uploads_urls)),
86 |
87 | url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),
88 | ]
89 |
90 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | amqp==1.4.5
2 | anyjson==0.3.3
3 | apns==2.0.1
4 | asn1crypto==0.24.0
5 | billiard==3.3.0.16
6 | bitarray==0.8.1
7 | boto==2.39.0
8 | celery==3.1.10
9 | certifi==2018.8.24
10 | cffi==1.11.5
11 | chardet==3.0.4
12 | click==6.7
13 | cryptography==2.2.2
14 | cssselect==0.9.1
15 | Django==1.8.19
16 | django-allauth==0.29.0
17 | django-bootstrap3==9.1.0
18 | django-debug-toolbar==1.2.1
19 | django-haystack==2.7.0
20 | django-rest-swagger==0.1.14
21 | django-ses==0.6.0
22 | django-taggit===0.21.1
23 | django-websocket-redis==0.4.1
24 | djangorestframework==2.4.8
25 | djangorestframework-jwt==0.1.4
26 | docutils==0.11
27 | drf-compound-fields==0.2.2
28 | drip==1.0.8
29 | elasticsearch==1.7.0
30 | enum34==1.1.6
31 | factory-boy==2.3.1
32 | gevent==1.3.3
33 | ghost==0.6.1
34 | Ghost.py==0.2.3
35 | greenlet==0.4.13
36 | idna==2.7
37 | ipaddress==1.0.22
38 | kombu==3.0.30
39 | lxml==3.3.6
40 | MySQL-python==1.2.5
41 | oauthlib==0.6.2
42 | paho-mqtt==1.3.1
43 | parse==1.8.4
44 | Pillow==2.4.0
45 | pyasn1==0.1.7
46 | pycparser==2.10
47 | pycrypto==2.6.1
48 | pyelasticsearch==1.4.1
49 | PyJWT==0.2.1
50 | pyOpenSSL==16.2.0
51 | pysolr==3.2.0
52 | python-dateutil==2.2
53 | pyfcm==1.4.7
54 | python-mpns==0.1.6
55 | python-openid==2.2.5
56 | pytz==2014.2
57 | redis==2.9.1
58 | requests==2.19.1
59 | requests-oauthlib==0.4.0
60 | rsa==3.1.4
61 | shortuuid==0.4.2
62 | simplejson==3.16.0
63 | six==1.11.0
64 | sqlparse==0.1.11
65 | South==0.8.4
66 | tinydb==3.9.0.post1
67 | ua-parser==0.8.0
68 | urllib3==1.23
69 | user-agents==1.1.0
70 | uWSGI==2.0.17
71 | Whoosh==2.6.0
72 |
--------------------------------------------------------------------------------
/templates/account/account_inactive.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %}
6 |
7 | {% block content %}
8 |
9 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/templates/account/base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 |
4 |
--------------------------------------------------------------------------------
/templates/account/email.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load i18n %}
4 | {% load url from future %}
5 |
6 | {% block head_title %}{% trans "Account" %}{% endblock %}
7 |
8 | {% block content %}
9 |
78 |
79 |
80 | {% endblock %}
81 |
82 |
83 | {% block extra_body %}
84 |
97 |
98 |
99 |
100 | {% endblock %}
101 |
--------------------------------------------------------------------------------
/templates/account/email/email_confirmation_message.txt:
--------------------------------------------------------------------------------
1 | {% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with current_site.name as site_name %}User {{ user_display }} at {{ site_name }} has given this as an email address.
2 |
3 | To confirm this is correct, go to {{ activate_url }}
4 | {% endblocktrans %}{% endautoescape %}
5 |
--------------------------------------------------------------------------------
/templates/account/email/email_confirmation_signup_message.txt:
--------------------------------------------------------------------------------
1 | {% include "account/email/email_confirmation_message.txt" %}
2 |
--------------------------------------------------------------------------------
/templates/account/email/email_confirmation_signup_subject.txt:
--------------------------------------------------------------------------------
1 | {% include "account/email/email_confirmation_subject.txt" %}
2 |
--------------------------------------------------------------------------------
/templates/account/email/email_confirmation_subject.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% autoescape off %}
3 | {% blocktrans %}Confirm E-mail Address{% endblocktrans %}
4 | {% endautoescape %}
5 |
--------------------------------------------------------------------------------
/templates/account/email/password_reset_key_message.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}{% blocktrans with site.domain as site_domain and user.username as username %}You're receiving this e-mail because you or someone else has requested a password for your user account at {{site_domain}}.
2 | It can be safely ignored if you did not request a password reset. Click the link below to reset your password.
3 |
4 | {{password_reset_url}}
5 |
6 | In case you forgot, your username is {{username}}.
7 |
8 | Thanks for using our site!
9 | {% endblocktrans %}
10 |
--------------------------------------------------------------------------------
/templates/account/email/password_reset_key_subject.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% autoescape off %}
3 | {% blocktrans %}Password Reset E-mail{% endblocktrans %}
4 | {% endautoescape %}
--------------------------------------------------------------------------------
/templates/account/email_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 |
4 | {% load url from future %}
5 | {% load i18n %}
6 | {% load account %}
7 |
8 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
9 |
10 |
11 | {% block content %}
12 |
13 |
14 |
44 |
45 |
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/templates/account/email_confirmed.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 |
4 | {% load i18n %}
5 | {% load account %}
6 | {% load url from future %}
7 |
8 | {% block head_title %}{% trans "Sign In" %}{% endblock %}
9 |
10 | {% block bootstrap3_extra_head %}
11 | {{ block.super }}
12 |
13 | {% endblock %}
14 |
15 |
16 | {% block content %}
17 |
18 | {% blocktrans %}If you have not created an account yet, then please
19 | sign up first.{% endblocktrans %}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
66 | {% if form.errors %}
67 |
Your username and password didn't match. Please try again.
68 | {% endif %}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {% endblock %}
82 |
--------------------------------------------------------------------------------
/templates/account/logout.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 |
6 | {% block head_title %}{% trans "Sign Out" %}{% endblock %}
7 |
8 | {% block content %}
9 | {% trans "Sign Out" %}
10 |
11 | {% trans 'Are you sure you want to sign out?' %}
12 |
13 |
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/templates/account/messages/cannot_delete_primary_email.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}You cannot remove your primary e-mail address ({{email}}).{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/messages/email_confirmation_sent.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}Confirmation e-mail sent to {{email}}.{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/messages/email_confirmed.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}You have confirmed {{email}}.{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/messages/email_deleted.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}Removed e-mail address {{email}}.{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/messages/logged_in.txt:
--------------------------------------------------------------------------------
1 | {% load account %}
2 | {% load i18n %}
3 | {% user_display user as name %}
4 | {% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %}
5 |
--------------------------------------------------------------------------------
/templates/account/messages/logged_out.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}You have signed out.{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/messages/password_changed.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}Password successfully changed.{% endblocktrans %}
3 |
4 |
--------------------------------------------------------------------------------
/templates/account/messages/password_set.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}Password successfully set.{% endblocktrans %}
3 |
4 |
--------------------------------------------------------------------------------
/templates/account/messages/primary_email_set.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}Primary e-mail address set.{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/messages/unverified_primary_email.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans %}Your primary e-mail address must be verified.{% endblocktrans %}
3 |
--------------------------------------------------------------------------------
/templates/account/password_change.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 |
6 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
30 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/templates/account/password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 |
4 | {% load i18n %}
5 | {% load account %}
6 | {% load url from future %}
7 |
8 | {% block head_title %}{% trans "Password Reset" %}{% endblock %}
9 |
10 | {% block content %}
11 |
12 |
43 |
44 |
45 | {% endblock %}
46 |
47 | {% block extra_body %}
48 |
51 | {% endblock %}
52 |
--------------------------------------------------------------------------------
/templates/account/password_reset_done.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
29 |
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/templates/account/password_reset_from_key.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
6 |
7 | {% block content %}
8 |
41 |
42 |
43 | {% endblock %}
44 |
--------------------------------------------------------------------------------
/templates/account/password_reset_from_key_done.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
6 |
7 | {% block content %}
8 |
9 |
23 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/templates/account/password_set.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 |
4 | {% load url from future %}
5 | {% load i18n %}
6 |
7 | {% block head_title %}{% trans "Set Password" %}{% endblock %}
8 |
9 | {% block content %}
10 |
11 |
29 |
30 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/templates/account/signup.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 |
6 | {% block head_title %}{% trans "Signup" %}{% endblock %}
7 |
8 | {% block bootstrap3_extra_head %}
9 | {{ block.super }}
10 |
11 | {% endblock %}
12 |
13 |
14 | {% block content %}
15 |
16 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {% endblock %}
64 |
65 |
66 |
--------------------------------------------------------------------------------
/templates/account/signup_closed.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 |
6 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
11 |
28 |
29 |
30 | {% endblock %}
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/templates/account/snippets/already_logged_in.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load account %}
3 |
4 | {% user_display user as user_display %}
5 |
6 | {% trans "Note" %}: {% blocktrans %}you are already logged in as {{ user_display }}.{% endblocktrans %}
7 |
--------------------------------------------------------------------------------
/templates/account/verification_sent.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
6 |
7 | {% block content %}
8 |
9 |
28 |
29 |
30 |
31 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/templates/account/verified_email_required.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% load url from future %}
4 | {% load i18n %}
5 |
6 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
38 |
39 |
40 |
41 | {% endblock %}
42 |
--------------------------------------------------------------------------------
/templates/email/email_subscribe_request.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% autoescape off %}
3 | {% blocktrans %}
4 |
5 | A device named {{ dev_name }} is asking to subscribe {{ channel_name }}, to authorize/reject request go to channel edit page at this
6 | url http://www.pushetta.com{{ channel_admin_url }} .
7 |
8 |
9 | Thanks for using Pushetta
10 |
11 | ------
12 | Follow Us on Facebook https://www.facebook.com/www.pushetta
13 |
14 |
15 | {% endblocktrans %}
16 | {% endautoescape %}
17 |
--------------------------------------------------------------------------------
/templates/rest_framework_swagger/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'www/__base.html' %}
2 |
3 | {% block title %}Pushetta API{% endblock %}
4 | {% load staticfiles %}
5 |
6 | {% block bootstrap3_extra_head %}
7 | {{ block.super }}
8 |
9 |
10 |
11 |
12 |
13 | {% endblock %}
14 |
15 | {% block content %}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {% endblock %}
25 |
26 | {% block bootstrap3_extra_script %}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
76 |
77 | {% endblock %}
--------------------------------------------------------------------------------
/templates/search/indexes/core/channel_text.txt:
--------------------------------------------------------------------------------
1 | {{ object.name }}
2 | {{ object.description }}
3 | {{ object.image }}
4 | {{ object.hidden }}
5 | {{ object.kind }}
6 | {{ object.subscriptions }}
7 |
--------------------------------------------------------------------------------
/templates/www/404.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 | {% block title %}Pushetta{% endblock %}
3 | {% block bootstrap3_extra_head %}
4 | {{ block.super }}
5 |
6 |
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
404
18 |
19 |
Who! bad trip man. No more pixels for you.
20 |
21 |
22 | Sorry, an error has occured! Why not try going back to the
home page
23 |
24 |
25 | {% if user.is_authenticated %}
26 |
33 | {% endif %}
34 |
35 |
36 |
37 |
38 |
39 |
40 | {% endblock %}
41 |
42 |
43 | {% block bootstrap3_extra_script %}
44 |
45 | {% endblock %}
46 |
47 |
--------------------------------------------------------------------------------
/templates/www/500.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 | {% block title %}Pushetta{% endblock %}
3 | {% block bootstrap3_extra_head %}
4 | {{ block.super }}
5 |
6 |
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
404
18 |
19 |
ARRGGG!! Something goes wrong.
20 |
21 |
22 | Every time someone get this page a developer cry, try to go to the
home page to stop this :-)
23 |
24 |
25 |
26 | {% if user.is_authenticated %}
27 |
34 | {% endif %}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {% endblock %}
42 |
43 |
44 | {% block bootstrap3_extra_script %}
45 |
46 | {% endblock %}
47 |
48 |
--------------------------------------------------------------------------------
/templates/www/__bootstrap.html:
--------------------------------------------------------------------------------
1 | {% extends 'bootstrap3/bootstrap3.html' %}
2 |
3 | {% block bootstrap3_title %}{% block title %}{% endblock %}{% endblock %}
--------------------------------------------------------------------------------
/templates/www/android-subscribe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Pushetta
7 |
10 |
11 |
12 |
13 |
19 |
35 |
36 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |

92 |
{{ channel.name }}
93 |
94 | {{ channel.description }}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
Or
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/templates/www/api.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% block title %}Pushetta{% endblock %}
4 |
5 | {% block bootstrap3_extra_head %}
6 | {{ block.super }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
40 |
41 |
42 | {% endblock %}
43 |
44 | {% block bootstrap3_extra_script %}
45 |
46 |
47 |
48 |
49 | {% endblock %}
--------------------------------------------------------------------------------
/templates/www/apps.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% block title %}Pushetta - Apps{% endblock %}
4 |
5 | {% block bootstrap3_extra_head %}
6 | {{ block.super }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
25 | {% endblock %}
26 |
27 | {% block bootstrap3_extra_script %}
28 | {% endblock %}
--------------------------------------------------------------------------------
/templates/www/chrome-subscribe.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% block title %}Pushetta - {{ channel.name }}{% endblock %}
4 | {% load storelink from storelink %}
5 | {% block bootstrap3_extra_head %}
6 | {{ block.super }}
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
38 | {% endblock %}
39 |
40 | {% block bootstrap3_extra_script %}
41 |
42 |
43 |
108 |
109 | {% endblock %}
--------------------------------------------------------------------------------
/templates/www/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% block title %}Pushetta{% endblock %}
4 |
5 | {% block bootstrap3_extra_head %}
6 | {{ block.super }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
23 |
24 | {% if user.channels.count == 0 %}
25 |
26 |
39 |
40 | {% else %}
41 |
80 |
81 |
82 |
Latest channels
83 |
84 | {% for latest in latest_channels %}
85 |
{{ latest.name }}
86 | {{ latest.date_created }}
87 |
88 |
89 | {% endfor %}
90 |
91 |
92 |
93 |
Latest subscribers
94 |
95 | {% for subscriber in subscribers %}
96 |
97 | {{ subscriber.name }}
98 |
99 | {{ subscriber.date_created }}
100 |
101 |
102 | {% endfor %}
103 |
104 |
105 |
106 | {% endif %}
107 |
108 | {% endblock %}
109 |
110 |
111 | {% block bootstrap3_extra_script %}
112 |
113 |
114 |
115 |
116 | {% endblock %}
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/templates/www/downloads.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 | {% block title %}Pushetta{% endblock %}
3 | {% block bootstrap3_extra_head %}
4 | {{ block.super }}
5 |
6 |
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
23 |
33 |
43 |
44 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
66 |
67 |
This App is BETA, to partecipate in beta testing write to guglielmino@gumino.com
68 |
69 |
70 |
71 |
72 |
73 |
74 | {% endblock %}
75 |
76 |
77 | {% block bootstrap3_extra_script %}
78 |
79 | {% endblock %}
80 |
81 |
--------------------------------------------------------------------------------
/templates/www/index.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 | {% block title %}Pushetta{% endblock %}
3 | {% block bootstrap3_extra_head %}
4 | {{ block.super }}
5 |
6 |
7 | {% endblock %}
8 | {% block content %}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
New house for Pushetta
42 |
43 | A BIG thank You to Utopía Soluciones Informáticas for supporting Us.
44 | Pushetta is now hosted in the server farm of Utopía and this is a big deal for our future.
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
67 |
68 |
69 |
70 |
71 | {% endblock %}
72 | {% block bootstrap3_extra_script %}
73 |
74 |
75 |
76 |
77 |
82 |
83 |
95 |
96 |
97 |
111 |
112 |
113 | {% endblock %}
114 |
--------------------------------------------------------------------------------
/templates/www/ios-subscribe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Pushetta
7 |
10 |
11 |
12 |
13 |
19 |
37 |
38 |
87 |
88 |
89 |
90 |
91 |
{{ channel.name }}
92 |
93 |
94 |
95 |
96 | {{ channel.description }}
97 |
98 |
99 |
100 |
101 |

102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/templates/www/partials/_messages_list.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/www/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /my/
--------------------------------------------------------------------------------
/templates/www/search.html:
--------------------------------------------------------------------------------
1 | {% extends '__base.html' %}
2 |
3 | {% block title %}Pushetta{% endblock %}
4 |
5 | {% block bootstrap3_extra_head %}
6 | {{ block.super }}
7 |
8 |
9 |
10 |
11 | {% endblock %}
12 |
13 | {% block content %}
14 |
15 |
27 |
28 |
29 | {% if suggested.count > 0 %}
30 | {% for channel in suggested %}
31 |
52 | {% endfor %}
53 | {% else %}
54 |
55 | No Channel found
56 |
57 | {% endif %}
58 |
59 |
60 |
61 | {% endblock %}
62 |
63 | {% block bootstrap3_extra_script %}
64 |
65 |
66 |
67 |
68 |
107 |
108 | {% endblock %}
--------------------------------------------------------------------------------