PKO]snitch/__init__.py"""Django app made to integrate generic events that create notifications that can be sent to users using several backends. By default, it integrates push notifications and email to send the notifications. """ from django.utils.module_loading import autodiscover_modules from snitch.decorators import register, dispatch from snitch.handlers import manager, EventHandler from snitch.helpers import explicit_dispatch, get_notification_model __all__ = [ "register", "manager", "EventHandler", "dispatch", "explicit_dispatch", "get_notification_model", ] __version__ = "1.0" def autodiscover(): autodiscover_modules("events", register_to=manager) default_app_config = "snitch.apps.SnitchConfig" PKOMLLsnitch/admin.pyfrom django.contrib import admin from django.utils.translation import ugettext_lazy as _ from snitch.models import EventType, Event, Notification def notify_action(modeladmin, request, queryset): """Explicit creates notifications for events.""" for event in queryset: event.notify() modeladmin.message_user(request, _("Events notified!")) notify_action.short_description = _("Notify events") def send_action(modeladmin, request, queryset): """Explicit sends the notifications using the backend.""" for notification in queryset: notification.send(send_async=True) modeladmin.message_user(request, _("Notifications sent!")) send_action.short_description = _("Send notifications") @admin.register(EventType) class EventTypeAdmin(admin.ModelAdmin): list_display = ["id", "verb", "enabled"] @admin.register(Event) class EventAdmin(admin.ModelAdmin): list_display = ["id", "actor", "verb", "trigger", "target", "notified", "created"] list_filter = ["verb", "notified"] actions = [notify_action] @admin.register(Notification) class NotificationAdmin(admin.ModelAdmin): list_display = ["id", "event", "user", "read", "received", "created"] list_filter = ["event__verb", "read", "sent"] search_fields = ["user__email"] actions = [send_action] autocomplete_fields = ["user"] PK O{snitch/apps.pyfrom django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ class SimpleSnitchConfig(AppConfig): """Simple AppConfig which does not do automatic discovery.""" name = "snitch" verbose_name = _("Snitch") class SnitchConfig(SimpleSnitchConfig): """The default AppConfig for admin which does automatic discovery.""" def ready(self): super().ready() self.module.autodiscover() PKO>^ snitch/backends.pyimport logging from django.contrib.auth import get_user_model from push_notifications.gcm import GCMError from push_notifications.models import GCMDevice, APNSDevice from snitch.emails import TemplateEmailMessage from snitch.settings import ENABLED_SEND_NOTIFICATIONS logger = logging.getLogger(__name__) User = get_user_model() class AbstractBackend: """Abstract backend class for notifications.""" def __init__(self, notification): self.notification = notification self.title = self.notification.event.title() self.text = self.notification.event.text() self.action = self.notification.event.action() def send(self): """A subclass should to implement the send method.""" raise NotImplementedError class PushNotificationBackend(AbstractBackend): """A backend class to send push notifications depending on the platform.""" def _send_android(self): devices = GCMDevice.objects.filter(user=self.notification.user) message = self.text extra = {} if self.title: extra["title"] = self.title if self.action: extra["click_action"] = self.action try: devices.send_message(message=message, extra=extra) except GCMError: logger.warning("Error sending push message") def _send_ios(self): devices = APNSDevice.objects.filter(user=self.notification.user) message = self.text extra = {} if self.title: message = {"title": self.title, "body": self.text} if self.action: extra["click_action"] = self.action try: devices.send_message(message=message, extra=extra) except GCMError: logger.warning("Error sending push message") def send(self): """Send message for each platform.""" if ENABLED_SEND_NOTIFICATIONS: self._send_android() self._send_ios() class EmailNotificationBackend(AbstractBackend): """Backend for using the email app to send emails.""" def send(self): """Sends the email.""" if ENABLED_SEND_NOTIFICATIONS: # Gets the handler to extract the arguments from template_email_kwargs handler = self.notification.event.handler() if hasattr(handler, "template_email_kwargs"): kwargs = handler.template_email_kwargs # Gets to email email = ( getattr(User, "EMAIL_FIELD") if hasattr(User, "EMAIL_FIELD") else None ) if email: kwargs.update( {"to": getattr(self.notification.user, "EMAIL_FIELD")} ) # Context context = kwargs.get("context", {}) context.update({"notification": self.notification}) kwargs.update({"context": context}) # Sends email email = TemplateEmailMessage(**kwargs) email.send( use_async=handler.template_email_async if hasattr(handler, "template_email_async") else True ) PK*jO snitch/decorators.pyfrom snitch.helpers import extract_actor_trigger_target def register(verb, verbose=None): """Decorator to register an event with its handler. @events.register("verb", _("verb verbose")) class Handler(events.EventHandler): pass """ from snitch.handlers import manager def _event_handler_wrapper(event_handler_class): manager.register(verb, event_handler_class, verbose=verbose) return event_handler_class return _event_handler_wrapper def dispatch(verb, method=False, config=None): """Decorator to dispatch an event when a method or function is called. The arguments attribute if to configure how to extract the actor, trigger and target from the decorated function arguments. config = { "args": ("actor", "trigger", "target") # The position determines de type "kwargs": {"actor": "", "trigger": "", "target": ""} } Example: @events.dispatch("verb") def method(self, trigger, target=None): # self is the actor pass """ from django.contrib.contenttypes.models import ContentType from snitch.models import Event, EventType from snitch.handlers import manager def _decorator(func): """Decorator itself.""" def _wrapper_trigger_action(*args, **kwargs): """Wrapped function with the decorator.""" # Calls the function and saves the result result = func(*args, **kwargs) # Check if verb is enabled or not if EventType.objects.filter(verb=verb, enabled=False).exists(): return result # Extract actor, trigger and target # If it isn't specified in arguments attribute, use the handler if config is None: handler_class = manager.handler_class(verb) actor, trigger, target = handler_class.extract_actor_trigger_target( method, *args, **kwargs ) # If it's explicit, use the arguments attribute else: if not isinstance(config, dict) or ( "args" not in config and "kwargs" not in config ): return result actor, trigger, target = extract_actor_trigger_target( config, args, kwargs ) # Creates the event if there is an actor if actor: event = Event( actor_content_type=ContentType.objects.get_for_model(actor), actor_object_id=actor.pk, verb=verb, ) if trigger and hasattr(trigger, "pk") and trigger.pk is not None: try: event.trigger_content_type = ContentType.objects.get_for_model( trigger ) event.trigger_object_id = trigger.pk except ContentType.DoesNotExist: pass if target and hasattr(target, "pk") and target.pk is not None: try: event.target_content_type = ContentType.objects.get_for_model( target ) event.target_object_id = target.pk except ContentType.DoesNotExist: pass event.save() return result return _wrapper_trigger_action return _decorator PK lO{4jCsnitch/emails.pyimport warnings import bleach from django.conf import settings from django.contrib.sites.models import Site from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from snitch.settings import ENABLED_SEND_NOTIFICATIONS from snitch.tasks import send_email_asynchronously class TemplateEmailMessage(object): """An object to handle emails based on templates, with automatic plain alternatives. """ default_template_name = "" default_subject = "" default_from_email = "" fake = False def __init__( self, to, subject=None, context=None, from_email=None, attaches=None, template_name=None, ): self.template_name = ( self.default_template_name if template_name is None else template_name ) if not self.template_name: warnings.warn("You have to specify the template name") if not isinstance(to, list) and not isinstance(to, tuple): self.to = [to] self.subject = "%s" % self.default_subject if subject is None else subject self.from_email = self.default_from_email if from_email is None else from_email self.attaches = [] if attaches is None else attaches self.default_context = {} if context is None else context def get_context(self): """Hook to customize context.""" # Add default context current_site = Site.objects.get_current() self.default_context.update({"site": current_site}) return self.default_context def preview(self): """Renders the message for a preview.""" context = self.get_context() message = render_to_string(self.template_name, context, using="django") return message def async_send(self, message, message_txt): if not self.fake: send_email_asynchronously.delay( self.subject, message_txt, message, self.from_email, self.to ) if self.attaches: warnings.warn( "Attaches will not added to the email, use async=False to send " "attaches." ) def sync_send(self, message, message_txt): if not self.fake: email = EmailMultiAlternatives( subject=self.subject, body=message_txt, from_email=self.from_email, to=self.to, ) email.attach_alternative(message, "text/html") for attach in self.attaches: attach_file_name, attach_content, attach_content_type = attach email.attach(attach_file_name, attach_content, attach_content_type) email.send() def send(self, use_async=True): """Sends the email at the moment or using a Celery task.""" if not ENABLED_SEND_NOTIFICATIONS: return context = self.get_context() message = render_to_string(self.template_name, context, using="django") message_txt = message.replace("\n", "") message_txt = message_txt.replace("

", "\n") message_txt = message_txt.replace("", "\n\n") message_txt = bleach.clean(message_txt, strip=True) if use_async: self.async_send(message, message_txt) else: self.sync_send(message, message_txt) class AdminsTemplateEmailMessage(TemplateEmailMessage): """Emails only for admins.""" def __init__(self, subject=None, context=None, from_email=None): to = [a[1] for a in settings.ADMINS] super().__init__(to, subject=subject, context=context, from_email=from_email) class ManagersTemplateEmailMessage(TemplateEmailMessage): """Emails only for mangers.""" def __init__(self, subject=None, context=None, from_email=None): to = [m[1] for m in settings.MANAGERS] super().__init__(to, subject=subject, context=context, from_email=from_email) PK O]OѶsnitch/exceptions.pyclass HandlerError(Exception): """An error configuring the event handler.""" pass class AlreadyRegistered(Exception): """Event handlers already register.""" pass PK OX snitch/handlers.pyfrom django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ from snitch.exceptions import HandlerError from snitch.helpers import extract_actor_trigger_target, get_notification_model class EventHandler: """Base event backend to generic even types.""" should_notify = True should_send = True dispatch_config = {"args": ("actor", "trigger", "target")} action = None title = None text = None notification_backends = [] @classmethod def extract_actor_trigger_target(cls, method, *args, **kwargs): """Extracts actor, trigger and target from the args and kwargs given as parameters. Override to implement a specific extractor. """ if not isinstance(cls.dispatch_config, dict) or ( "args" not in cls.dispatch_config and "kwargs" not in cls.dispatch_config ): raise HandlerError(_("The dispatch config is incorrect.")) return extract_actor_trigger_target( config=cls.dispatch_config, args=args, kwargs=kwargs ) def __init__(self, event): self.event = event def _default_dynamic_text(self): """Makes an event human readable.""" text = "{} {}".format(str(self.event.actor), self.event.verb) if self.event.trigger: text = "{} {}".format(text, str(self.event.trigger)) if self.event.target: text = "{} {}".format(text, str(self.event.target)) return text def get_text(self): """Override to handle different human readable implementations.""" return self.text or self._default_dynamic_text() def get_title(self): """Gets the title for the event. To be hooked.""" return self.title def get_action(self): """Gets the action depending on the verb. To be hooked.""" return self.action def audience(self): """Gets the audience of the event. None by default, to be hooked by the user.""" User = get_user_model() return User.objects.none() def notify(self): """Creates a notification fot each user in the audience.""" Notification = get_notification_model() for user in self.audience(): notification = Notification(event=self.event, user=user) notification.save() class EventManager: """The event manager in the responsible of handling the registration of the handlers with the verbs. """ def __init__(self): self._registry = {} self._verbs = {} def register(self, verb, handler, verbose=None): """Register a handler with a verb, and the verbose form of the verb.""" if not issubclass(handler, EventHandler): raise HandlerError( _(f"The handler {handler} have to inherit from EventHandler.") ) self._verbs[verb] = verbose if verbose else verb self._registry[verb] = handler def choices(self): """Gets a tuple of tuples with the registers verbs and its verbose form, to be used as choices.""" return tuple(self._verbs.items()) def handler_class(self, verb): """Returns a class instance of the handler for the given verb.""" return self._registry.get(verb) def handler(self, event): """Returns an instance of the handler for the given event.""" return self.handler_class(event.verb)(event) # This global object represents the singleton event manager object manager = EventManager() PKkO۔OOsnitch/helpers.pyfrom django.apps import apps as django_apps from django.core.exceptions import ImproperlyConfigured from snitch.settings import NOTIFICATION_MODEL def get_notification_model(): """Return the Notification model that is active in this project.""" try: return django_apps.get_model(NOTIFICATION_MODEL, require_ready=False) except ValueError: raise ImproperlyConfigured( "NOTIFICATION_MODEL must be of the form 'app_label.model_name'" ) except LookupError: raise ImproperlyConfigured( "NOTIFICATION_MODEL refers to model '%s' that has not been installed" % NOTIFICATION_MODEL ) def explicit_dispatch(verb, config=None, *args, **kwargs): """Helper to explicit dispatch an event without using a decorator.""" from snitch.decorators import dispatch return dispatch(verb, config)(lambda *args, **kwargs: None)(*args, **kwargs) def extract_actor_trigger_target(config, args, kwargs): """Extracts the actor, trigger and target using the arguments config given from the generic arguments args and kwargs. """ actor = trigger = target = None if "args" in config: arguments_args = config.get("args", tuple()) try: actor = args[arguments_args.index("actor")] except (ValueError, IndexError): pass try: trigger = args[arguments_args.index("trigger")] except (ValueError, IndexError): pass try: target = args[arguments_args.index("target")] except (ValueError, IndexError): pass if "kwargs" in config: arguments_kwargs = config.get("kwargs", dict()) try: actor = kwargs[arguments_kwargs.get("actor")] except (ValueError, KeyError): pass try: trigger = kwargs[arguments_kwargs.get("trigger")] except (ValueError, KeyError): pass try: target = kwargs[arguments_kwargs.get("target")] except (ValueError, KeyError): pass return actor, trigger, target PKO=>>snitch/models.pyfrom django.conf import settings from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import ugettext_lazy as _ from model_utils.models import TimeStampedModel from snitch.handlers import manager User = get_user_model() class EventType(models.Model): """Explicit model for Event types, represented by the event verb. It's used to enable or disable the generation of notifications. """ verb = models.CharField( max_length=255, null=True, choices=manager.choices(), unique=True ) enabled = models.BooleanField(default=True, verbose_name=_("enabled")) class Meta: verbose_name = _("event type") verbose_name_plural = _("event types") ordering = ("verb",) def __str__(self): return str(self.verb) class Event(TimeStampedModel): """A 'event' is generated when an 'actor' performs 'verb', involving 'action', in the 'target'. It could be: Reference: http://activitystrea.ms/specs/atom/1.0/ """ actor_content_type = models.ForeignKey( ContentType, related_name="actor_actions", null=True, on_delete=models.CASCADE, verbose_name=_("actor content type"), ) actor_object_id = models.PositiveIntegerField(_("actor object id"), null=True) actor = GenericForeignKey("actor_content_type", "actor_object_id") verb = models.CharField( _("verb"), max_length=255, null=True, choices=manager.choices() ) trigger_content_type = models.ForeignKey( ContentType, related_name="trigger_actions", blank=True, null=True, on_delete=models.CASCADE, verbose_name=_("trigger content type"), ) trigger_object_id = models.PositiveIntegerField( _("trigger object id"), blank=True, null=True ) trigger = GenericForeignKey("trigger_content_type", "trigger_object_id") target_content_type = models.ForeignKey( ContentType, related_name="target_actions", blank=True, null=True, on_delete=models.CASCADE, verbose_name=_("target content type"), ) target_object_id = models.PositiveIntegerField( _("target object id"), blank=True, null=True ) target = GenericForeignKey("target_content_type", "target_object_id") notified = models.BooleanField(_("notified"), default=False) class Meta: verbose_name = _("event") verbose_name_plural = _("events") def __str__(self): return self.text() def handler(self): """Gets the handler for the event. Save the instance of the handler in the model. """ if not hasattr(self, "_handler_instance"): self._handler_instance = manager.handler(self) return self._handler_instance def text(self): """Gets the human readable text for the event.""" handler = self.handler() return handler.get_text() def title(self): """Gets the title for the event.""" handler = self.handler() return handler.get_title() def action(self): """Gets the action depending on the verb.""" handler = self.handler() return handler.get_action() def notify(self): """Creates the notifications associated to this action, .""" handler = self.handler() if handler.should_notify: handler.notify() self.notified = True self.save() def save(self, *args, **kwargs): result = super().save(*args, **kwargs) if not self.notified: self.notify() return result class AbstractNotification(TimeStampedModel): """A notification is sent to an user, and it's always related with an event.""" event = models.ForeignKey( Event, verbose_name=_("event"), related_name="notifications", on_delete=models.CASCADE, ) user = models.ForeignKey( settings.AUTH_USER_MODEL, related_name="notifications", on_delete=models.CASCADE ) sent = models.BooleanField(_("sent"), default=False) received = models.BooleanField(_("received"), default=False) read = models.BooleanField(_("read"), default=False) class Meta: verbose_name = _("notification") verbose_name_plural = _("notifications") ordering = ("-created",) abstract = True def __str__(self): return "'{}' to {}".format(str(self.event), str(self.user)) def send(self, send_async=False): """Sends a push notification to the devices of the user.""" from .tasks import push_task handler = self.event.handler() if handler.should_send: if send_async: push_task.delay(self.pk) else: for backend_class in handler.notification_backends: backend = backend_class(self) backend.send() self.sent = True self.save() def save(self, *args, **kwargs): """Overwrite to sending push notifications when saving.""" is_insert = self._state.adding super().save(*args, **kwargs) if is_insert: self.send() class Notification(AbstractNotification): """Initial notification model that can be swappable.""" class Meta(AbstractNotification.Meta): swappable = "SNITCH_NOTIFICATION_MODEL" PKO@iF411snitch/settings.pyfrom django.conf import settings # Needed to build and publish with Flit # ------------------------------------------------------------------------------ SECRET_KEY = "snitch" # Specific project configuration # ------------------------------------------------------------------------------ ENABLED_SEND_NOTIFICATIONS = getattr( settings, "SNITCH_ENABLED_SEND_NOTIFICATIONS", True ) ENABLED_SEND_EMAILS = getattr(settings, "SNITCH_ENABLED_SEND_EMAILS", True) NOTIFICATION_MODEL = getattr( settings, "SNITCH_NOTIFICATION_MODEL", "snitch.Notification" ) PK Oig)OOsnitch/tasks.pyfrom celery.task import task from django.core.mail import EmailMultiAlternatives @task(serializer="json") def push_task(notification_pk): """A Celery task to send push notifications related with a given Notification model.""" from snitch.events import get_notification_model Notification = get_notification_model() try: notification = Notification.objects.get(pk=notification_pk) except Notification.DoesNotExist: return False notification.push(send_async=False) @task(serializer="json") def send_email_asynchronously(subject, message_txt, message, from_email, to): """Sends an email as a asynchronous task.""" email = EmailMultiAlternatives( subject=subject, body=message_txt, from_email=from_email, to=to ) email.attach_alternative(message, "text/html") email.send() PK1O&hq )snitch/locale/es_ES/LC_MESSAGES/django.po# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-08-14 16:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: snitch/admin.py:11 msgid "Events notified!" msgstr "¡Eventos notificados!" #: snitch/admin.py:14 msgid "Notify events" msgstr "Notificar eventos" #: snitch/admin.py:21 msgid "Notifications sent!" msgstr "¡Notificaciones enviadas!" #: snitch/admin.py:24 msgid "Send notifications" msgstr "Enviar notificaciones" #: snitch/apps.py:9 msgid "Snitch" msgstr "Snitch" #: snitch/handlers.py:27 msgid "The dispatch config is incorrect." msgstr "La configuración de envío es incorrecta." #: snitch/handlers.py:82 #, python-brace-format msgid "The handler {handler} have to inherit from EventHandler." msgstr "El manejador {handler} ha de heredar de EventHandler." #: snitch/models.py:22 msgid "enabled" msgstr "habilitado" #: snitch/models.py:25 msgid "event type" msgstr "tipo de evento" #: snitch/models.py:26 msgid "event types" msgstr "tipos de evento" #: snitch/models.py:50 msgid "actor content type" msgstr "tipo de actor" #: snitch/models.py:52 msgid "actor object id" msgstr "ID de actor" #: snitch/models.py:56 msgid "verb" msgstr "verbo" #: snitch/models.py:65 msgid "trigger content type" msgstr "tipo de disparador" #: snitch/models.py:68 msgid "trigger object id" msgstr "ID de disparador" #: snitch/models.py:78 msgid "target content type" msgstr "tipo de objetivo" #: snitch/models.py:81 msgid "target object id" msgstr "ID de objetivo" #: snitch/models.py:85 msgid "notified" msgstr "notificado" #: snitch/models.py:88 snitch/models.py:137 msgid "event" msgstr "evento" #: snitch/models.py:89 msgid "events" msgstr "eventos" #: snitch/models.py:144 msgid "sent" msgstr "enviado" #: snitch/models.py:145 msgid "received" msgstr "recibido" #: snitch/models.py:146 msgid "read" msgstr "leído" #: snitch/models.py:149 msgid "notification" msgstr "notificación" #: snitch/models.py:150 msgid "notifications" msgstr "notificaciones" PKOZ*!snitch/migrations/0001_initial.py# Generated by Django 2.2.4 on 2019-08-14 14:55 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("contenttypes", "0002_remove_content_type_name"), ] operations = [ migrations.CreateModel( name="EventType", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("verb", models.CharField(max_length=255, null=True, unique=True)), ("enabled", models.BooleanField(default=True, verbose_name="enabled")), ], options={ "verbose_name": "event type", "verbose_name_plural": "event types", "ordering": ("verb",), }, ), migrations.CreateModel( name="Event", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "created", model_utils.fields.AutoCreatedField( default=django.utils.timezone.now, editable=False, verbose_name="created", ), ), ( "modified", model_utils.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False, verbose_name="modified", ), ), ( "actor_object_id", models.PositiveIntegerField( null=True, verbose_name="actor object id" ), ), ( "verb", models.CharField(max_length=255, null=True, verbose_name="verb"), ), ( "trigger_object_id", models.PositiveIntegerField( blank=True, null=True, verbose_name="trigger object id" ), ), ( "target_object_id", models.PositiveIntegerField( blank=True, null=True, verbose_name="target object id" ), ), ( "notified", models.BooleanField(default=False, verbose_name="notified"), ), ( "actor_content_type", models.ForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="actor_actions", to="contenttypes.ContentType", verbose_name="actor content type", ), ), ( "target_content_type", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="target_actions", to="contenttypes.ContentType", verbose_name="target content type", ), ), ( "trigger_content_type", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="trigger_actions", to="contenttypes.ContentType", verbose_name="trigger content type", ), ), ], options={"verbose_name": "event", "verbose_name_plural": "events"}, ), migrations.CreateModel( name="Notification", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "created", model_utils.fields.AutoCreatedField( default=django.utils.timezone.now, editable=False, verbose_name="created", ), ), ( "modified", model_utils.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False, verbose_name="modified", ), ), ("sent", models.BooleanField(default=False, verbose_name="sent")), ( "received", models.BooleanField(default=False, verbose_name="received"), ), ("read", models.BooleanField(default=False, verbose_name="read")), ( "event", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="notifications", to="snitch.Event", verbose_name="event", ), ), ( "user", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="notifications", to=settings.AUTH_USER_MODEL, ), ), ], options={ "verbose_name": "notification", "verbose_name_plural": "notifications", "ordering": ("-created",), "abstract": False, "swappable": "SNITCH_NOTIFICATION_MODEL", }, ), ] PKOsnitch/migrations/__init__.pyPKm Oua88#django_snitch-1.0.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2019 Marcos Gabarda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!HPO!django_snitch-1.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!Ht$django_snitch-1.0.dist-info/METADATAW[6~PCXM7m6LvH2NdY J2ȲͲ$-ɢs;sq5vRC̻t8C|+\ř&S+\z9,İA8MQ@kXSJFSֈHjD\h25H- GZj8l2#"j},3-y.ghRi`gJ|)]unYH3.*1PSBc*SFA$FtN\SZj s?b׾AYS#: rh:ΨqJ,e!?<83 {jٗh`t|-_l8P7*q+P%+?i`K?7O\: "#"eH n2;e| gEdGyA8 vFGRI[MrZx{J.= <6C)%2$""Q0 3Nr0#)bG=QhЏW)}7>MM HSU)Snx0ZXonk,)̭2j6[a{Lyqfڷ*w}tGjMk N(\3sPyhtˋgd^Ԧ.Y5W;Z,G+>0]]>MQMT{y7GIVx\B=*2AOp 9}X+qE}s[)s.Z0^S%JP}8- ڤ}kQaƑLmT$8D"#x8q 6Mv6sm k&muh&8/9Mbi!ö +#P0A!Hjj]ͮgjvXo7X3-_PK!HP"django_snitch-1.0.dist-info/RECORD}ɒJ}? ts &D2OCqw*lHN_Q=(loDid-/9p@\Fd VnMޫ Z迥8aO$Sd. Nmv4,4|hÅpi n&~g{4f6ż[Ŏr=#ܦ$ SI]E~B$}߫^AYVKOZ5ͪC I8iМ߱ZZbFJa $ͽN¤) '̞|+/SMA c'b #6yAC?ӴGȌ. -MHYQs'Qݮ} Nq|vde)ֿx%тP#w| Oi뎡-d8Z;0uͱژ݈%hO&?n*ȳ/}*ٲvK*k fɜ¤3ZmApCVߩM)vX}]ɝiʍ1hF9b/ޙt묳i7f]2wܭ$\Hz1{aHd_M }-R=Oulɑi˟C~Fm-kQ"șP<ӥ~Ie_)# @dz|k/ڸ∜" 6`l,]"T/isK^LFQ$gxt[Tj Ԙ7>ǍeidV!+O}0`7|}B,,W}̆YRAPn.-UPKQL8g+/u\.qP7͵T_TD_|%Δ,x OE\ y!:4E4+*/PKO]snitch/__init__.pyPKOMLLsnitch/admin.pyPK O{snitch/apps.pyPKO>^ h snitch/backends.pyPK*jO ^snitch/decorators.pyPK lO{4jC%snitch/emails.pyPK O]OѶf5snitch/exceptions.pyPK OX N6snitch/handlers.pyPKkO۔OO^Dsnitch/helpers.pyPKO=>>Lsnitch/models.pyPKO@iF411Hcsnitch/settings.pyPK Oig)OOesnitch/tasks.pyPK1O&hq )%isnitch/locale/es_ES/LC_MESSAGES/django.poPKOZ*!rsnitch/migrations/0001_initial.pyPKOԍsnitch/migrations/__init__.pyPKm Oua88#django_snitch-1.0.dist-info/LICENSEPK!HPO!django_snitch-1.0.dist-info/WHEELPK!Ht$django_snitch-1.0.dist-info/METADATAPK!HP".django_snitch-1.0.dist-info/RECORDPK(