コード例 #1
0
ファイル: admin.py プロジェクト: kiwix/cardshop
class ProfileForm(forms.Form):
    organization = forms.ChoiceField(choices=get_orgs)
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    username = forms.CharField(max_length=100)
    password = forms.CharField(max_length=100)
    is_admin = forms.BooleanField(initial=False,
                                  label=_lz("admin?"),
                                  required=False)
    can_sd = forms.BooleanField(initial=False,
                                label=_lz("SD?"),
                                required=False)

    @staticmethod
    def success_message(result):
        return _("Successfuly created Manager User <em>%(user)s</em>") % {
            "user": result
        }

    def clean_username(self):
        if Profile.exists(username=self.cleaned_data.get("username")):
            raise forms.ValidationError(_("Username is already taken."),
                                        code="invalid")
        return self.cleaned_data.get("username")

    def clean_email(self):
        if Profile.taken(email=self.cleaned_data.get("email")):
            raise forms.ValidationError(_("Email is already in use."),
                                        code="invalid")
        return self.cleaned_data.get("email")

    def clean_organization(self):
        if Organization.get_or_none(
                self.cleaned_data.get("organization")) is None:
            raise forms.ValidationError(_("Not a valid Organization"),
                                        code="invalid")
        return self.cleaned_data.get("organization")

    def save(self):
        if not self.is_valid():
            raise ValueError(
                _("%(class)s is not valid") % {"class": type(self)})

        organization = Organization.get_or_none(
            self.cleaned_data.get("organization"))
        return Profile.create(
            organization=organization,
            first_name=self.cleaned_data.get("name").strip(),
            email=self.cleaned_data.get("email"),
            username=self.cleaned_data.get("username"),
            password=self.cleaned_data.get("password"),
            is_admin=self.cleaned_data.get("is_admin"),
            can_order_physical=self.cleaned_data.get("can_sd"),
            expiry=None,
        )
コード例 #2
0
class PasswordResetForm(forms.Form):
    code = forms.UUIDField(label=_lz("Validation Code"))
    password = forms.CharField(max_length=50, label=_lz("New Password"))

    def clean_code(self):
        prc = PasswordResetCode.get_or_none(self.cleaned_data.get("code"))
        if not prc:
            raise forms.ValidationError(_("Invalid validation code"),
                                        code="invalid")
        if prc.created_on + datetime.timedelta(days=1) < timezone.now():
            raise forms.ValidationError(_("Expired validation code"),
                                        code="invalid")
        return prc
コード例 #3
0
class AuthenticationForm(DjangoAuthForm):
    """Authentication form."""

    otp_auth = forms.CharField(label=_lz("2 FA Code"),
                               max_length=6,
                               required=False)

    def __new__(cls, *args, **kwargs):
        """Class creation control."""
        instance = super(AuthenticationForm, cls).__new__(cls)
        instance.error_messages["invalid_login"] = _lz(
            "Please enter a correct %(username)s, password, and 2FA code. "
            "Note that either or both fields may be case-sensitive.")
        return instance

    def clean(self):
        """Clean the data."""
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        otp = self.cleaned_data.get("otp_auth")

        if username and password:
            self.user_cache = authenticate(username=username,
                                           password=password,
                                           otp_auth=otp)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
            else:
                self.confirm_login_allowed(self.user_cache)
        return self.cleaned_data
コード例 #4
0
 def __new__(cls, *args, **kwargs):
     """Class creation control."""
     instance = super(AuthenticationForm, cls).__new__(cls)
     instance.error_messages["invalid_login"] = _lz(
         "Please enter a correct %(username)s, password, and 2FA code. "
         "Note that either or both fields may be case-sensitive.")
     return instance
コード例 #5
0
class EmailForm(forms.Form):
    email = forms.EmailField(label=_lz("E-mail address"))

    def clean_email(self):
        try:
            return Profile.get_using(self.cleaned_data.get("email"))
        except Exception:
            raise forms.ValidationError(_("No account for e-mail"),
                                        code="invalid")
コード例 #6
0
class OrderShippingForm(forms.Form):
    def __init__(self, *args, **kwargs):
        order = kwargs.pop("order")
        super().__init__(*args, **kwargs)
        self.order = order

    details = forms.CharField(
        help_text=_lz("Shipment tracking details or similar"))

    def save(self, *args, **kwargs):
        success, response = add_order_shipment(
            order_id=self.order.scheduler_id,
            shipment_details=self.cleaned_data.get("details"),
        )
        if not success:
            raise SchedulerAPIError(response)
コード例 #7
0
 class Meta:
     ordering = ["slug"]
     verbose_name = _lz("organization")
     verbose_name_plural = _lz("organizations")
コード例 #8
0
 class Meta:
     unique_together = (("kind", "size"), )
     ordering = ["size"]
     verbose_name = _lz("media")
     verbose_name_plural = _lz("medias")
コード例 #9
0
 class Meta:
     ordering = ("-id", )
     verbose_name = _lz("address")
     verbose_name_plural = _lz("addresss")
コード例 #10
0
class Media(models.Model):

    PHYSICAL = "physical"
    VIRTUAL = "virtual"
    KINDS = {PHYSICAL: "Physical", VIRTUAL: "Virtual"}
    EXPIRATION_DELAY = 14

    class Meta:
        unique_together = (("kind", "size"), )
        ordering = ["size"]
        verbose_name = _lz("media")
        verbose_name_plural = _lz("medias")

    name = models.CharField(max_length=50, verbose_name=_lz("Name"))
    kind = models.CharField(max_length=50,
                            choices=KINDS.items(),
                            verbose_name=_lz("Kind"))
    size = models.BigIntegerField(help_text=_lz("In GB"),
                                  verbose_name=_lz("Size"))
    actual_size = models.BigIntegerField(
        help_text=_lz("In bytes (auto calc)"),
        blank=True,
        verbose_name=_lz("Actual size"),
    )
    units_coef = models.FloatField(verbose_name=_lz("Units"),
                                   help_text=_lz("How much units per GB"))

    def save(self, *args, **kwargs):
        self.actual_size = self.get_bytes()
        return super(Media, self).save(*args, **kwargs)

    @staticmethod
    def choices_for(items, display_units=True):
        return [
            (item.id,
             f"{item.name} ({item.units}U)" if display_units else item.name)
            for item in items
        ]

    @classmethod
    def get_or_none(cls, mid):
        try:
            return cls.objects.get(id=mid)
        except cls.DoesNotExist:
            return None

    @classmethod
    def get_choices(cls, kind=None, display_units=True):
        qs = cls.objects.all()
        if kind is not None:
            qs = qs.filter(kind=kind)
        return cls.choices_for(qs, display_units=display_units)

    @classmethod
    def get_min_for(cls, size):
        matching = [
            media for media in cls.objects.all() if media.bytes >= size
        ]
        return matching[0] if len(matching) else None

    def __str__(self):
        return self.name

    @property
    def bytes(self):
        return self.actual_size or self.get_bytes()

    def get_bytes(self):
        return get_hardware_adjusted_image_size(self.size * ONE_GB)

    @property
    def human(self):
        return human_readable_size(self.size * ONE_GB, False)

    @property
    def units(self):
        return self.size * self.units_coef

    @property
    def verbose_kind(self):
        return self.KINDS.get(self.kind)

    def get_duration_for(self, quantity=1):
        return self.EXPIRATION_DELAY * quantity
コード例 #11
0
CARDSHOP_API_URL_EXTERNAL = os.getenv(
    "CARDSHOP_API_URL_EXTERNAL", "https://api.cardshop.hotspot.kiwix.org"
)
# Token for API allowing creation of user accounts
ACCOUNTS_API_TOKEN = os.getenv(
    "ACCOUNTS_API_TOKEN", "dev"
)
# email-sending related (mailgun API)
MAIL_FROM = os.getenv("MAIL_FROM", "*****@*****.**")
MAILGUN_API_URL = os.getenv("MAILGUN_API_URL",
                            "https://api.mailgun.net/v3/cardshop.hotspot.kiwix.org")
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "")
# used for sending reset password links in emails
CARDSHOP_PUBLIC_URL = os.getenv("CARDSHOP_PUBLIC_URL",
                                "https://cardshop.hotspot.kiwix.org")
CONTENTS_FILE = os.path.join(BASE_DIR, "contents.json")

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": os.path.join(DATA_DIR, "cache"),
        "TIMEOUT": int(os.getenv("CACHE_TIMEOUT", "86400")),
        "OPTIONS": {"MAX_ENTRIES": 1000},
    }
}

LANGUAGES = [
  ('en', _lz('English')),
  ('fr', _lz('French')),
]
コード例 #12
0
class Address(models.Model):
    class Meta:
        ordering = ("-id", )
        verbose_name = _lz("address")
        verbose_name_plural = _lz("addresss")

    COUNTRIES = collections.OrderedDict(
        sorted([(c.alpha_2, c.name) for c in pycountry.countries],
               key=lambda x: x[1]))

    organization = models.ForeignKey(Organization,
                                     on_delete=models.CASCADE,
                                     verbose_name=_lz("Organization"))
    created_by = models.ForeignKey(
        "Profile",
        on_delete=models.CASCADE,
        related_name="created_addresses",
        verbose_name=_lz("Created addrresses"),
    )
    name = models.CharField(
        max_length=100,
        verbose_name=_lz("Address Name"),
        help_text=_lz("Used only within the Cardshop"),
    )
    recipient = models.CharField(max_length=100,
                                 verbose_name=_lz("Recipient Name"))
    email = models.EmailField(max_length=255, verbose_name=_lz("Email"))
    phone = models.CharField(
        null=True,
        blank=True,
        max_length=30,
        help_text=_lz("In international “+” format"),
        verbose_name=_lz("Phone"),
    )
    address = models.TextField(
        null=True,
        blank=True,
        help_text=_lz("Complete address without name and country"),
        verbose_name=_lz("Address"),
    )
    country = models.CharField(
        max_length=50,
        null=True,
        blank=True,
        choices=COUNTRIES.items(),
        verbose_name=_lz("Country"),
    )

    @classmethod
    def get_or_none(cls, aid):
        try:
            return cls.objects.get(id=aid)
        except cls.DoesNotExist:
            return None

    def save(self, *args, **kwargs):
        self.phone = self.cleaned_phone(
            self.phone) if self.phone is not None else None
        super().save(*args, **kwargs)

    @classmethod
    def get_choices(cls, organization):
        return [(item.id, item.name)
                for item in cls.objects.filter(organization=organization)]

    @staticmethod
    def country_name_for(country_code):
        return Address.COUNTRIES.get(country_code)

    @property
    def physical_compatible(self):
        return (self.phone is not None and self.address is not None
                and self.country is not None)

    @property
    def verbose_country(self):
        return self.country_name_for(self.country)

    @property
    def language(self):
        langs = babel.languages.get_official_languages(
            self.country.upper() if self.country else None)
        avail_langs = [code for code, name in settings.LANGUAGES]
        for lang in langs:
            if lang in avail_langs:
                return lang
        return settings.LANGUAGE_CODE

    @property
    def human_phone(self):
        if self.phone is None:
            return None
        return phonenumbers.format_number(
            phonenumbers.parse(self.phone, None),
            phonenumbers.PhoneNumberFormat.INTERNATIONAL,
        )

    @staticmethod
    def cleaned_phone(number):
        pn = phonenumbers.parse(number, None)
        if not phonenumbers.is_possible_number(pn):
            raise ValueError("Phone Number not possible")
        return phonenumbers.format_number(pn,
                                          phonenumbers.PhoneNumberFormat.E164)

    def to_payload(self):
        return {
            "name": self.recipient,
            "email": self.email,
            "phone": self.human_phone,
            "address": self.address,
            "country": self.country,
            "shipment": None,
        }

    def __str__(self):
        return self.name
コード例 #13
0
class Order(models.Model):
    NOT_CREATED = "not-created"
    IN_PROGRESS = "in-progress"
    FAILED = "failed"
    CANCELED = "canceled"
    COMPLETED = "completed"
    STATUSES = {
        IN_PROGRESS: "In Progress",
        COMPLETED: "Completed",
        FAILED: "Failed",
        CANCELED: "Canceled",
        NOT_CREATED: "Not accepted by Scheduler",
    }

    class Meta:
        ordering = ["-created_on"]
        verbose_name = _lz("order")
        verbose_name_plural = _lz("orders")

    organization = models.ForeignKey(Organization,
                                     on_delete=models.CASCADE,
                                     verbose_name=_lz("Organization"))
    created_on = models.DateTimeField(auto_now_add=True,
                                      verbose_name=_lz("Created on"))
    created_by = models.ForeignKey(Profile,
                                   on_delete=models.SET_NULL,
                                   null=True,
                                   verbose_name=_lz("Created by"))

    scheduler_id = models.CharField(max_length=50,
                                    unique=True,
                                    blank=True,
                                    verbose_name=_lz("Scheduler ID"))
    scheduler_data = jsonfield.JSONField(
        load_kwargs={"object_pairs_hook": collections.OrderedDict},
        null=True,
        blank=True,
        verbose_name=_lz("Scheduler Data"),
    )
    scheduler_data_on = models.DateTimeField(
        null=True, blank=True, verbose_name=_lz("Scheduler Data On"))
    status = models.CharField(
        max_length=50,
        choices=STATUSES.items(),
        default=IN_PROGRESS,
        verbose_name=_lz("Status"),
    )

    # copy of request data for archive purpose
    channel = models.CharField(max_length=50, verbose_name=_lz("Channel"))
    client_name = models.CharField(max_length=100,
                                   verbose_name=_lz("Client name"))
    client_email = models.EmailField(verbose_name=_lz("Client Email"))
    client_limited = models.BooleanField(default=True,
                                         verbose_name=_lz("Client is limited"))
    client_language = models.CharField(
        max_length=2,
        verbose_name=_lz("Client language"),
        choices=settings.LANGUAGES,
        default=settings.LANGUAGE_CODE,
    )
    config = jsonfield.JSONField(
        load_kwargs={"object_pairs_hook": collections.OrderedDict},
        verbose_name=_lz("Config"),
    )
    media_name = models.CharField(max_length=50,
                                  verbose_name=_lz("Media name"))
    media_type = models.CharField(max_length=50,
                                  verbose_name=_lz("Media type"))
    media_duration = models.IntegerField(blank=True,
                                         null=True,
                                         verbose_name=_lz("Media duration"))
    media_size = models.BigIntegerField(verbose_name=_lz("Media size"))
    quantity = models.IntegerField(verbose_name=_lz("Quantity"))
    units = models.IntegerField(verbose_name=_lz("Units"))
    recipient_name = models.CharField(max_length=100,
                                      verbose_name=_lz("Recipient name"))
    recipient_email = models.EmailField(verbose_name=_lz("Recipient Email"))
    recipient_language = models.CharField(
        max_length=2,
        verbose_name=_lz("Recipient language"),
        choices=settings.LANGUAGES,
        default=settings.LANGUAGE_CODE,
    )
    recipient_phone = models.CharField(max_length=50,
                                       blank=True,
                                       null=True,
                                       verbose_name=_lz("Recipient phone"))
    recipient_address = models.TextField(verbose_name=_lz("Recipient Address"))
    recipient_country_code = models.CharField(
        max_length=3, verbose_name=_lz("Recipient Country Code"))
    warehouse_upload_uri = models.CharField(
        max_length=255, verbose_name=_lz("Warehouse Upload URI"))
    warehouse_download_uri = models.CharField(
        max_length=255, verbose_name=_lz("Warehouse Download URI"))

    @classmethod
    def fetch_and_get(cls, order_id):
        order = cls.objects.get(id=order_id)
        # fetch current version of scheduler data
        retrieved, scheduler_data = get_order(order.scheduler_id)
        if retrieved:
            order.retrieved = True
            order.scheduler_data = scheduler_data
            order.scheduler_data_on = timezone.now()
            # update status fron scheduler data
            order.status = Order.status_from_statuses(
                scheduler_data.get("statuses"))
            order.save()
        else:
            order.retrieved = False
        return order

    @staticmethod
    def status_from_statuses(statuses):
        if not isinstance(statuses, list) or not statuses:
            return Order.FAILED

        status = statuses[-1].get("status")
        if "failed" in status:
            return Order.FAILED
        if status in ("canceled", "timedout"):
            return Order.FAILED
        if status in ("shipped", "pending_expiry"):
            return Order.COMPLETED

        return Order.IN_PROGRESS

    @classmethod
    def get_or_none(cls, min_id):
        try:
            local_id = re.match(r"^L([0-9]+)R", min_id).groups()[0]
        except KeyError:
            return None
        except cls.DoesNotExist:
            return None
        else:
            return cls.fetch_and_get(local_id)

    @classmethod
    def get_by_scheduler_id(cls, scheduler_id):
        try:
            return cls.objects.get(scheduler_id=scheduler_id)
        except cls.DoesNotExist:
            return None

    @classmethod
    def _submit_provisionned_order(cls, order, skip_units=False):
        if not skip_units:
            if (order.created_by.is_limited
                    and order.units > order.created_by.organization.units):
                raise ValueError(
                    _("Order requires %(req_u)dU but %(org)s has only %(avail_u)d"
                      ) % {
                          "req_u": order.units,
                          "org": order.created_by.organization,
                          "avail_u": order.created_by.organization.units,
                      })
            if order.created_by.is_limited:
                # remove units from org
                order.created_by.organization.units -= order.units
                order.created_by.organization.save()

        payload = order.to_payload()
        created, scheduler_id = create_order(payload)
        if not created:
            logger.error(scheduler_id)
            # restore units on org
            if not skip_units and order.created_by.is_limited:
                order.created_by.organization.units += order.units
                order.created_by.organization.save()
            raise SchedulerAPIError(scheduler_id)
        order.scheduler_id = scheduler_id
        order.save()
        return order

    @classmethod
    def create_from(cls,
                    client,
                    config,
                    media,
                    quantity,
                    address=None,
                    request_lang=None):
        if not address and media.kind != Media.VIRTUAL:
            raise ValueError(_("Non-virtual order requires an address"))
        country_code = (address.country if address is not None else None) or ""
        client_language = client.get_language(request_lang)
        warehouse = client.organization.get_warehouse_details(
            use_public=media.kind == Media.VIRTUAL)
        order = cls(
            organization=client.organization,
            created_by=client,
            channel=client.organization.channel,
            client_name=client.name,
            client_email=client.email,
            client_language=client_language,
            client_limited=client.is_limited,
            config=config.json,
            media_name=media.name,
            media_type=media.kind,
            media_size=media.size,
            media_duration=media.get_duration_for(quantity),
            quantity=quantity,
            units=media.units * quantity,
            recipient_name=address.recipient if address else client.name,
            recipient_email=address.email if address else client.email,
            recipient_language=address.language
            if address else client_language,
            recipient_phone=address.phone if address else "",
            recipient_address=address.address if address else "",
            recipient_country_code=country_code,
            warehouse_upload_uri=warehouse["upload_uri"],
            warehouse_download_uri=warehouse["download_uri"],
        )

        return Order._submit_provisionned_order(order)

    @classmethod
    def profile_has_active(cls, client):
        for order in cls.objects.filter(created_by=client,
                                        status=cls.IN_PROGRESS):
            order = cls.fetch_and_get(order.id)
            if order.status == cls.IN_PROGRESS:
                return order.min_id
        return False

    def recreate(self):
        order = Order.fetch_and_get(self.id)
        if not order.data.can_recreate:
            raise ValueError(_("Unable to recreate order (cancel first?)."))

        order.id = None
        order.scheduler_id = None
        order.scheduler_data = None
        order.created_on = None
        order.scheduler_data_on = None
        order.status = self.IN_PROGRESS

        return Order._submit_provisionned_order(order,
                                                skip_units=order.status
                                                in (self.COMPLETED,
                                                    self.IN_PROGRESS))

    @property
    def data(self):
        return OrderData(self.scheduler_data or self.to_payload())

    @property
    def active(self):
        return self.status == self.IN_PROGRESS

    @property
    def min_id(self):
        return "L{dj}R{sched}".format(dj=self.id,
                                      sched=self.scheduler_id[:4]).upper()

    @property
    def short_id(self):
        return self.scheduler_id[:8] + self.scheduler_id[-3:]

    @property
    def config_json(self):
        return json.loads(self.config)

    @property
    def verbose_status(self):
        return self.STATUSES.get(self.status)

    def __str__(self):
        return "Order #{id}/{sid}".format(id=self.id, sid=self.scheduler_id)

    def to_payload(self):
        return {
            "config": self.config_json,
            "sd_card": {
                "name": self.media_name,
                "size": self.media_size,
                "type": self.media_type,
                "duration": self.media_duration,
            },
            "quantity": self.quantity,
            "units": self.units,
            "client": {
                "name": self.client_name,
                "email": self.client_email,
                "limited": self.client_limited,
                "language": self.client_language,
            },
            "recipient": {
                "name": self.recipient_name,
                "email": self.recipient_email,
                "language": self.recipient_language,
                "phone": self.recipient_phone,
                "address": self.recipient_address,
                "country": self.recipient_country_code,
                "shipment": None,
            },
            "channel": self.channel,
            "warehouse": {
                "upload_uri": self.warehouse_upload_uri,
                "download_uri": self.warehouse_download_uri,
            },
        }

    def cancel(self):
        """manually cancel an order"""
        canceled, resp = cancel_order(self.scheduler_id)
        if canceled:
            self.status = self.CANCELED
            self.save()
        else:
            logger.error(resp)
        return canceled

    def anonymize(self):
        redacted = "[ANONYMIZED]"
        self.scheduler_data = {}
        self.scheduler_data_on = timezone.now()
        self.client_name = redacted
        self.client_email = "anonymized.tld"
        self.recipient_name = redacted
        self.recipient_email = self.client_email
        self.recipient_phone = redacted
        self.recipient_address = redacted
        self.save()
コード例 #14
0
 class Meta:
     ordering = ["-created_on"]
     verbose_name = _lz("order")
     verbose_name_plural = _lz("orders")
コード例 #15
0
class Organization(models.Model):
    class Meta:
        ordering = ["slug"]
        verbose_name = _lz("organization")
        verbose_name_plural = _lz("organizations")

    slug = models.SlugField(primary_key=True, verbose_name=_lz("Slug"))
    name = models.CharField(max_length=100, verbose_name=_lz("Name"))
    language = models.CharField(
        max_length=2,
        verbose_name=_lz("Language"),
        choices=settings.LANGUAGES,
        blank=True,
        null=True,
    )
    channel = models.CharField(
        max_length=50,
        choices=get_channel_choices(),
        default="kiwix",
        verbose_name=_lz("Channel"),
    )
    warehouse = models.CharField(
        max_length=50,
        choices=get_warehouse_choices(),
        default="kiwix",
        verbose_name=_lz("Warehouse"),
    )
    public_warehouse = models.CharField(
        max_length=50,
        choices=get_warehouse_choices(),
        default="download",
        verbose_name=_("Pub WH"),
    )
    email = models.EmailField(verbose_name=_lz("Email"))
    units = models.IntegerField(null=True,
                                blank=True,
                                default=0,
                                verbose_name=_lz("Units"))

    @property
    def is_limited(self):
        return self.units is not None

    @classmethod
    def get_or_none(cls, slug):
        try:
            return cls.objects.get(slug=slug)
        except cls.DoesNotExist:
            return None

    @classmethod
    def create_kiwix(cls):
        if cls.objects.filter(slug="kiwix").count():
            return cls.objects.get(slug="kiwix")
        return cls.objects.create(slug="kiwix",
                                  name="Kiwix",
                                  email="*****@*****.**",
                                  units=256000)

    def get_warehouse_details(self, use_public=False):
        success, warehouse = get_warehouse_from(
            self.public_warehouse if use_public else self.warehouse)
        if not success:
            raise SchedulerAPIError(warehouse)
        return warehouse

    def __str__(self):
        return self.name
コード例 #16
0
 class Meta:
     get_latest_by = "-id"
     ordering = ["-id"]
     verbose_name = _lz("configuration")
     verbose_name_plural = _lz("configurations")
コード例 #17
0
class Configuration(models.Model):
    class Meta:
        get_latest_by = "-id"
        ordering = ["-id"]
        verbose_name = _lz("configuration")
        verbose_name_plural = _lz("configurations")

    KALITE_LANGUAGES = ["en", "fr", "es"]
    WIKIFUNDI_LANGUAGES = ["en", "fr", "es"]

    organization = models.ForeignKey(
        "Organization",
        on_delete=models.CASCADE,
        related_name="configurations",
        verbose_name=_lz("configurations"),
    )
    updated_on = models.DateTimeField(
        auto_now=True,
        verbose_name=_lz("updated on"),
    )
    updated_by = models.ForeignKey(
        "Profile",
        on_delete=models.CASCADE,
        related_name="configurations",
        verbose_name=_lz("configurations"),
    )
    size = models.BigIntegerField(
        blank=True,
        verbose_name=_lz("Size"),
    )

    name = models.CharField(
        verbose_name=_lz("Same"),
        max_length=100,
        help_text=_lz("Used <strong>only within the Cardshop</strong>"),
    )
    project_name = models.CharField(
        max_length=64,
        default="kiwix",
        verbose_name=_lz("Hospot name"),
        help_text=_lz(
            "Network name; the landing page will also be at http://name.hotspot"
        ),
        validators=[validate_project_name],
    )
    language = models.CharField(
        max_length=3,
        choices=hotspot_languages,
        default="en",
        verbose_name=_lz("Language"),
        help_text=_lz("Hotspot interface language"),
        validators=[validate_language],
    )
    timezone = models.CharField(
        max_length=75,
        choices=[("UTC", "UTC"), ("Europe/Paris", "Europe/Paris")] +
        [(tz, tz) for tz in pytz.common_timezones],
        default="Europe/Paris",
        verbose_name=_lz("Timezone"),
        help_text=_lz("Where the Hotspot would be deployed"),
        validators=[validate_timezone],
    )

    wifi_password = models.CharField(
        max_length=31,
        default=None,
        verbose_name=_lz("WiFi Password"),
        help_text=_lz(
            "Leave empty for Open WiFi (recommended)"
            "<br />Do <strong>not</strong> use special characters. 8 chars min."
        ),
        null=True,
        blank=True,
        validators=[validate_wifi_pwd],
    )
    admin_account = models.CharField(
        max_length=31,
        default="admin",
        validators=[validate_admin_login],
        verbose_name=_lz("Admin account"),
    )
    admin_password = models.CharField(
        max_length=31,
        default="admin-password",
        verbose_name=_lz("Admin password"),
        help_text=_lz("To manage KA-Lite, Aflatoun, EduPi and Wikifundi"),
        validators=[validate_admin_pwd],
    )

    branding_logo = models.FileField(blank=True,
                                     null=True,
                                     upload_to=get_branding_path,
                                     verbose_name=_lz("Logo"))
    branding_favicon = models.FileField(blank=True,
                                        null=True,
                                        upload_to=get_branding_path,
                                        verbose_name=_lz("Favicon"))
    branding_css = models.FileField(blank=True,
                                    null=True,
                                    upload_to=get_branding_path,
                                    verbose_name=_lz("CSS File"))

    content_zims = jsonfield.JSONField(
        blank=True,
        null=True,
        load_kwargs={"object_pairs_hook": collections.OrderedDict},
        default="",
    )
    content_kalite_fr = models.BooleanField(
        default=False,
        verbose_name=_lz("Khan Academy FR"),
        help_text=_lz("Learning Platform (French)"),
    )
    content_kalite_en = models.BooleanField(
        default=False,
        verbose_name=_lz("Khan Academy EN"),
        help_text=_lz("Learning Platform (English)"),
    )
    content_kalite_es = models.BooleanField(
        default=False,
        verbose_name=_lz("Khan Academy ES"),
        help_text=_lz("Learning Platform (Spanish)"),
    )
    content_wikifundi_fr = models.BooleanField(
        default=False,
        verbose_name=_lz("WikiFundi FR"),
        help_text=_lz("Wikipedia-like Editing Platform (French)"),
    )
    content_wikifundi_en = models.BooleanField(
        default=False,
        verbose_name=_lz("WikiFundi EN"),
        help_text=_lz("Wikipedia-like Editing Platform (English)"),
    )
    content_wikifundi_es = models.BooleanField(
        default=False,
        verbose_name=_lz("WikiFundi ES"),
        help_text=_lz("Wikipedia-like Editing Platform (Spanish)"),
    )
    content_aflatoun = models.BooleanField(
        default=False,
        verbose_name=_lz("Aflatoun"),
        help_text=_lz("Education Platform for kids"),
    )
    content_edupi = models.BooleanField(
        default=False,
        verbose_name=_lz("EduPi"),
        help_text=_lz("Share arbitrary files with all users"),
    )
    content_edupi_resources = models.CharField(
        max_length=500,
        blank=True,
        null=True,
        verbose_name=_lz("EduPi Resources"),
        help_text=_lz(
            "ZIP folder archive of documents to initialize EduPi with"),
    )
    content_nomad = models.BooleanField(
        default=False,
        verbose_name=_lz("Nomad android apps"),
        help_text=_lz("Révisions du CP à la 3è"),
    )
    content_mathews = models.BooleanField(
        default=False,
        verbose_name=_lz("Math Mathews android"),
        help_text=_lz("Un jeu pour réviser les maths"),
    )
    content_africatik = models.BooleanField(
        default=False,
        verbose_name=_lz("Africatik apps"),
        help_text=_lz("Applications éducatives pour l'Afrique"),
    )

    @classmethod
    def create_from(cls, config, author):

        # only packages IDs which are in the catalogs
        packages_list = get_list_if_values_match(
            get_nested_key(config, ["content", "zims"]), get_packages_id())
        # list of requested langs for kalite
        kalite_langs = get_list_if_values_match(
            get_nested_key(config, ["content", "kalite"]),
            cls.KALITE_LANGUAGES)
        # list of requested langs for wikifundi
        wikifundi_langs = get_list_if_values_match(
            get_nested_key(config, ["content", "wikifundi"]),
            cls.WIKIFUNDI_LANGUAGES)

        # branding
        logo = extract_branding(config, "logo", ["image/png"])
        favicon = extract_branding(config, "favicon",
                                   ["image/x-icon", "image/png"])
        css = extract_branding(config, "css", ["text/css", "text/plain"])

        # name is used twice
        name = get_if_str(
            get_nested_key(config, "project_name"),
            cls._meta.get_field("project_name").default,
        )

        # wifi
        wifi_password = None
        # wifi (previous format)
        if "wifi" in config and isinstance(config["wifi"], dict):
            if "password" in config["wifi"].keys() and config["wifi"].get(
                    "protected", True):
                wifi_password = get_if_str(
                    get_nested_key(config, ["wifi", "password"]))
        # wifi (new format)
        if "wifi_password" in config.keys():
            wifi_password = config["wifi_password"]

        # rebuild clean config from data
        kwargs = {
            "updated_by":
            author,
            "organization":
            author.organization,
            "name":
            name,
            "project_name":
            name,
            "language":
            get_if_str_in(
                get_nested_key(config, "language"),
                dict(cls._meta.get_field("language").choices).keys(),
            ),
            "timezone":
            get_if_str_in(
                get_nested_key(config, "timezone"),
                dict(cls._meta.get_field("timezone").choices).keys(),
            ),
            "wifi_password":
            wifi_password,
            "admin_account":
            get_if_str(
                get_nested_key(config, ["admin_account", "login"]),
                cls._meta.get_field("admin_account").default,
            ),
            "admin_password":
            get_if_str(
                get_nested_key(config, ["admin_account", "password"]),
                cls._meta.get_field("admin_password").default,
            ),
            "branding_logo":
            save_branding_file(logo) if logo is not None else None,
            "branding_favicon":
            save_branding_file(favicon) if favicon is not None else None,
            "branding_css":
            save_branding_file(css) if css is not None else None,
            "content_zims":
            packages_list,
            "content_kalite_fr":
            "fr" in kalite_langs,
            "content_kalite_en":
            "en" in kalite_langs,
            "content_kalite_es":
            "es" in kalite_langs,
            "content_wikifundi_fr":
            "fr" in wikifundi_langs,
            "content_wikifundi_en":
            "en" in wikifundi_langs,
            "content_wikifundi_es":
            "es" in wikifundi_langs,
            "content_aflatoun":
            bool(get_nested_key(config, ["content", "aflatoun"])),
            "content_edupi":
            bool(get_nested_key(config, ["content", "edupi"])),
            "content_edupi_resources":
            get_if_str(get_nested_key(config, ["content", "edupi_resources"])),
            "content_nomad":
            bool(get_nested_key(config, ["content", "nomad"])),
            "content_mathews":
            bool(get_nested_key(config, ["content", "mathews"])),
            "content_africatik":
            bool(get_nested_key(config, ["content", "africatik"])),
        }

        try:
            return cls.objects.create(**kwargs)
        except Exception as exp:
            logger.warn(exp)

            # remove saved branding files
            for key in ("branding_logo", "branding_favicon", "branding_css"):
                if kwargs.get(key):
                    try:
                        Path(settings.MEDIA_ROOT).joinpath(kwargs.get(key))
                    except FileNotFoundError:
                        pass
            raise exp

    def duplicate(self, by):
        kwargs = {}
        for field in self._meta.fields:
            kwargs[field.name] = getattr(self, field.name)
        kwargs.pop("id")
        kwargs["name"] = "{} (Duplicate)".format(kwargs.get("name", ""))
        kwargs["updated_by"] = by
        new_instance = self.__class__(**kwargs)
        new_instance.save()

        return new_instance

    def size_value_changed(self):
        computed_size = get_required_image_size(self.collection)
        if computed_size != self.size:
            self.size = computed_size
            return True
        return False

    def save(self, *args, **kwargs):
        # remove packages not in catalog
        self.content_zims = [
            package for package in self.content_zims
            if package in get_packages_id()
        ]
        self.size_value_changed()
        super().save(*args, **kwargs)

    def retrieve_missing_zims(self):
        """checks packages list over catalog for changes"""
        return [
            package for package in self.content_zims
            if package not in get_packages_id()
        ]

    @classmethod
    def get_choices(cls, organization):
        return [(
            item.id,
            "{name} ({date})".format(name=item.display_name,
                                     date=item.updated_on.strftime("%c")),
        ) for item in cls.objects.filter(organization=organization)
                if not item.retrieve_missing_zims()]

    @classmethod
    def get_or_none(cls, aid):
        try:
            return cls.objects.get(id=aid)
        except cls.DoesNotExist:
            return None

    @property
    def wifi_protected(self):
        return bool(self.wifi_password)

    @property
    def display_name(self):
        return self.name or self.project_name

    @property
    def json(self):
        return json.dumps(self.to_dict(), indent=4)

    @property
    def min_media(self):
        return Media.get_min_for(self.size)

    def can_fit_on(self, media):
        return media.bytes >= self.size

    def compatible_medias(self):
        return Media.objects.filter(actual_size__gte=self.size)
        # return [m for m in Media.objects.all() if m.bytes >= self.size]

    @property
    def min_units(self):
        return self.min_media.units

    @property
    def kalite_languages(self):
        return [
            lang for lang in self.KALITE_LANGUAGES
            if getattr(self, "content_kalite_{}".format(lang), False)
        ]

    @property
    def wikifundi_languages(self):
        return [
            lang for lang in self.WIKIFUNDI_LANGUAGES
            if getattr(self, "content_wikifundi_{}".format(lang), False)
        ]

    def all_languages(self):
        return self._meta.get_field("language").choices

    def __str__(self):
        return self.display_name

    @property
    def collection(self):
        return get_collection(
            edupi=self.content_edupi,
            edupi_resources=self.content_edupi_resources or None,
            nomad=self.content_nomad,
            mathews=self.content_mathews,
            africatik=self.content_africatik,
            packages=self.content_zims or [],
            kalite_languages=self.kalite_languages,
            wikifundi_languages=self.wikifundi_languages,
            aflatoun_languages=["fr", "en"] if self.content_aflatoun else [],
        )

    def to_dict(self):
        # for key in ("project_name", "language", "timezone"):
        #     config.append((key, getattr(self, key)))
        return collections.OrderedDict([
            ("name", self.name),
            ("project_name", self.project_name),
            ("language", self.language),
            ("timezone", self.timezone),
            ("wifi_password", self.wifi_password),
            (
                "admin_account",
                collections.OrderedDict([
                    ("login", self.admin_account),
                    ("password", self.admin_password),
                ]),
            ),
            ("size", self.min_media.human),
            (
                "content",
                collections.OrderedDict([
                    ("zims", self.content_zims),
                    ("kalite", self.kalite_languages),
                    ("wikifundi", self.wikifundi_languages),
                    ("aflatoun", self.content_aflatoun),
                    ("edupi", self.content_edupi),
                    ("edupi_resources", self.content_edupi_resources),
                    ("nomad", self.content_nomad),
                    ("mathews", self.content_mathews),
                    ("africatik", self.content_africatik),
                ]),
            ),
            (
                "branding",
                collections.OrderedDict([
                    ("logo", retrieve_branding_file(self.branding_logo)),
                    ("favicon", retrieve_branding_file(self.branding_favicon)),
                    ("css", retrieve_branding_file(self.branding_css)),
                ]),
            ),
        ])
コード例 #18
0
class OrderForm(forms.Form):
    KIND_CHOICES = {
        Media.VIRTUAL: _lz("Download Link"),
        Media.PHYSICAL: _lz("Physical micro-SD Card(s)"),
    }

    # VALIDITY_CHOICES = {x: "{} days".format(x * 5) for x in range(1, 11)}

    def __init__(self, *args, **kwargs):
        request = kwargs.pop("request")
        super().__init__(*args, **kwargs)
        self.client = request.user.profile
        self.request_lang = get_language_from_request(request)
        self.organization = self.client.organization
        self.fields["config"].choices = Configuration.get_choices(
            self.organization)
        self.fields["address"].choices = [
            ("none", "Myself")
        ] + Address.get_choices(self.organization)
        self.fields["media"].choices = Media.get_choices(
            kind=None if self.client.can_order_physical else Media.VIRTUAL,
            display_units=self.client.is_limited,
        )
        self.fields["kind"].choices = filter(
            lambda item: self.client.can_order_physical or item[0] != Media.
            PHYSICAL,
            self.KIND_CHOICES.items(),
        )

    kind = forms.ChoiceField(
        choices=[],
        label=_lz("Order Type"),
        help_text=_lz(
            "Either download link sent via email or micro-SD card shipment"),
    )
    config = forms.ChoiceField(choices=[], label=_lz("Configuration"))
    address = forms.ChoiceField(choices=[], label=_lz("Recipient"))
    media = forms.ChoiceField(
        choices=[],
        help_text=_lz(
            "You can choose larger media size to add free space to your hotspot"
        ),
    )
    quantity = forms.IntegerField(
        initial=1,
        min_value=1,
        max_value=10,
        help_text=_lz("Number of physical micro-SD cards you want"),
    )

    def VIRTUAL_CHOICES(self):
        return Media.get_choices(kind=Media.VIRTUAL)

    def PHYSICAL_CHOICES(self):
        return Media.get_choices(kind=Media.PHYSICAL)

    def clean_config(self):
        config = Configuration.get_or_none(self.cleaned_data.get("config"))
        if config is None or config.organization != self.organization:
            raise forms.ValidationError(_("Not your configuration"),
                                        code="invalid")
        return config

    def clean_address(self):
        if self.cleaned_data.get("address", "none") == "none":
            return
        address = Address.get_or_none(self.cleaned_data.get("address"))
        if address and address.organization != self.organization:
            raise forms.ValidationError(_("Not your address"), code="invalid")
        return address

    def clean_media(self):
        media = Media.get_or_none(self.cleaned_data.get("media"))
        if media is None:
            raise forms.ValidationError(_("Incorrect Media"), code="invalid")
        if media.kind == Media.PHYSICAL and not self.client.can_order_physical:
            raise forms.ValidationError(_("Not allowed to order physical"),
                                        code="invalid")
        return media

    def clean_quantity(self):
        if self.cleaned_data.get("kind") == Media.VIRTUAL:
            return 1
        try:
            quantity = int(self.cleaned_data.get("quantity"))
        except Exception:
            raise forms.ValidationError(_("Incorrect quantity"),
                                        code="invalid")
        return quantity

    def clean(self):
        cleaned_data = super().clean()
        config = cleaned_data.get("config")
        media = cleaned_data.get("media")
        kind = cleaned_data.get("kind")
        address = cleaned_data.get("address")

        if kind == Media.PHYSICAL and (not address
                                       or not address.physical_compatible):
            self.add_error(
                "address",
                _("This address can't be used as it misses postal details"))

        if config is not None:
            # save config if its size changed
            if config.size_value_changed():
                config.save()

        if config is not None and media is not None and not config.can_fit_on(
                media):
            min_media = Media.get_min_for(config.size)
            if min_media is None:
                msg = _("There is no large enough Media for this config.")
                field = "config"
            else:
                msg = _(
                    "Media not large enough for config (use at least %(media)s)"
                ) % {
                    "media": min_media.name
                }
                field = "media"
            self.add_error(field, msg)

    def save(self, *args, **kwargs):
        return Order.create_from(client=self.client,
                                 config=self.cleaned_data.get("config"),
                                 media=self.cleaned_data.get("media"),
                                 quantity=self.cleaned_data.get("quantity"),
                                 address=self.cleaned_data.get("address"),
                                 request_lang=self.request_lang).min_id

    @classmethod
    def success_message(cls, res):
        return _("Successfuly created Order <em>%(order)s</em>") % {
            "order": res
        }
コード例 #19
0
class Profile(models.Model):
    class Meta:
        ordering = ["organization", "user__username"]
        verbose_name = _lz("profile")
        verbose_name_plural = _lz("profiles")

    user = models.OneToOneField(User,
                                on_delete=models.CASCADE,
                                verbose_name=_lz("User"))
    language = models.CharField(
        max_length=2,
        verbose_name=_lz("Language"),
        choices=settings.LANGUAGES,
        blank=True,
        null=True,
    )
    organization = models.ForeignKey(Organization,
                                     on_delete=models.CASCADE,
                                     verbose_name=_lz("Organization"))
    can_order_physical = models.BooleanField(
        default=False, verbose_name=_lz("Can order physical?"))
    expire_on = models.DateTimeField(blank=True,
                                     null=True,
                                     verbose_name=_lz("Expire on"))

    @property
    def is_limited(self):
        if self.user.is_staff:
            return False
        return self.organization.is_limited

    @property
    def username(self):
        return self.user.username

    @property
    def email(self):
        return self.user.email

    @classmethod
    def get_or_none(cls, username):
        try:
            return cls.objects.get(user__username=username)
        except (cls.DoesNotExist, User.DoesNotExist):
            return None

    @classmethod
    def get_using(cls, email):
        return cls.objects.get(user__email=email)

    @classmethod
    def create_admin(cls):
        organization = Organization.create_kiwix()

        if User.objects.filter(username="******").count():
            user = User.objects.get(username="******")
        else:
            user = User.objects.create_superuser(
                username="******",
                first_name="John",
                last_name="Doe",
                email=organization.email,
                password=settings.ADMIN_PASSWORD,
            )
        if cls.objects.filter(user=user).count():
            return cls.objects.get(user=user)

        return cls.objects.create(user=user,
                                  organization=organization,
                                  can_order_physical=True)

    @classmethod
    def exists(cls, username):
        return bool(User.objects.filter(username=username).count())

    @classmethod
    def taken(cls, email):
        return bool(User.objects.filter(email=email).count())

    @classmethod
    def create(
        cls,
        organization,
        first_name,
        email,
        username,
        password,
        is_admin,
        expiry,
        can_order_physical,
    ):
        if cls.exists(username) or cls.taken(email):
            raise ValueError(_("Profile parameters non unique"))

        user = User.objects.create_user(
            username=username,
            email=email,
            password=password,
            first_name=first_name,
            is_staff=is_admin,
            is_superuser=is_admin,
        )

        try:
            if expiry and not expiry.tzinfo:
                expiry = expiry.astimezone(timezone.utc)
            return cls.objects.create(
                user=user,
                organization=organization,
                can_order_physical=is_admin or can_order_physical,
                expire_on=expiry,
            )
        except Exception as exp:
            logger.error(exp)
            # make sure we remove the User object so it can be recreated later
            user.delete()
            raise exp

    @property
    def name(self):
        return self.user.get_full_name()

    def get_language(self, request_lang=None):
        if self.language:
            return self.language

        if self.organization.language:
            return self.organization.language

        if request_lang:
            request_lang = request_lang.split("-")[0]
            if request_lang in [code for code, name in settings.LANGUAGES]:
                return request_lang

        return settings.LANGUAGE_CODE

    def __str__(self):
        return "{user} ({org})".format(user=self.name,
                                       org=str(self.organization))
コード例 #20
0
 class Meta:
     ordering = ["organization", "user__username"]
     verbose_name = _lz("profile")
     verbose_name_plural = _lz("profiles")
コード例 #21
0
                             "https://api.cardshop.hotspot.kiwix.org")
CARDSHOP_API_URL_EXTERNAL = os.getenv(
    "CARDSHOP_API_URL_EXTERNAL", "https://api.cardshop.hotspot.kiwix.org")
# Token for API allowing creation of user accounts
ACCOUNTS_API_TOKEN = os.getenv("ACCOUNTS_API_TOKEN", "dev")
# email-sending related (mailgun API)
MAIL_FROM = os.getenv("MAIL_FROM", "*****@*****.**")
MAILGUN_API_URL = os.getenv(
    "MAILGUN_API_URL", "https://api.mailgun.net/v3/cardshop.hotspot.kiwix.org")
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "")
# used for sending reset password links in emails
CARDSHOP_PUBLIC_URL = os.getenv("CARDSHOP_PUBLIC_URL",
                                "https://cardshop.hotspot.kiwix.org")
CONTENTS_FILE = os.path.join(BASE_DIR, "contents.json")

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": os.path.join(DATA_DIR, "cache"),
        "TIMEOUT": int(os.getenv("CACHE_TIMEOUT", "3600")),
        "OPTIONS": {
            "MAX_ENTRIES": 1000
        },
    }
}

LANGUAGES = [
    ("en", _lz("English")),
    ("fr", _lz("French")),
]