class SitePlatformSettings(TranslatableModel, BasePlatformSettings): contact_email = models.EmailField(null=True, blank=True) contact_phone = models.CharField(max_length=100, null=True, blank=True) copyright = models.CharField(max_length=100, null=True, blank=True) powered_by_text = models.CharField(max_length=100, null=True, blank=True) powered_by_link = models.CharField(max_length=100, null=True, blank=True) powered_by_logo = models.ImageField( null=True, blank=True, upload_to='site_content/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) logo = models.FileField( null=True, blank=True, upload_to='site_content/', validators=[ FileExtensionValidator(allowed_extensions=['svg']), validate_file_infection ] ) favicon = models.ImageField( null=True, blank=True, upload_to='site_content/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) translations = TranslatedFields( metadata_title=models.CharField( max_length=100, null=True, blank=True), metadata_description=models.TextField( null=True, blank=True), metadata_keywords=models.CharField( max_length=300, null=True, blank=True), start_page=models.CharField( max_length=100, null=True, blank=True, help_text=_('Slug of the start initiative page') ), ) class Meta: verbose_name_plural = _('site platform settings') verbose_name = _('site platform settings')
class PictureItem(ContentItem): """ Picture content item """ class PictureAlignment(DjangoChoices): float_left = ChoiceItem('float-left', label=_("Float left")) center = ChoiceItem('center', label=_("Center")) float_right = ChoiceItem('float-right', label=_("Float right")) image = ImageField( _("Picture"), upload_to='content_images', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) align = models.CharField(_("Align"), max_length=50, choices=PictureAlignment.choices) class Meta(object): verbose_name = _("Picture") verbose_name_plural = _("Pictures") def __str__(self): return self.image.name if self.image else u'(no image)'
class File(AnonymizationMixin, models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) created = models.DateField(_('created'), default=timezone.now) file = models.FileField( _('file'), upload_to='files', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.PRIVATE_FILE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) owner = models.ForeignKey( 'members.Member', verbose_name=_('owner'), related_name='own_%(class)s', ) used = models.BooleanField(_('used'), default=False) def __str__(self): return str(self.id) class JSONAPIMeta(object): resource_name = 'files' class Meta(object): abstract = True
class Slide(SortableMixin, models.Model): block = models.ForeignKey('cms.SlidesContent', related_name='slides') tab_text = models.CharField( _("Tab text"), max_length=100, help_text=_("This is shown on tabs beneath the banner.") ) title = models.CharField(_("Title"), max_length=100, blank=True) body = models.TextField(_("Body text"), blank=True) image = ImageField( _("Image"), max_length=255, blank=True, null=True, upload_to='banner_slides/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) background_image = ImageField( _("Background image"), max_length=255, blank=True, null=True, upload_to='banner_slides/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) video_url = models.URLField( _("Video url"), max_length=100, blank=True, default='' ) link_text = models.CharField( _("Link text"), max_length=400, help_text=_("This is the text on the button inside the banner."), blank=True ) link_url = models.CharField( _("Link url"), max_length=400, help_text=_("This is the link for the button inside the banner."), blank=True ) sequence = models.PositiveIntegerField(default=0, editable=False, db_index=True) class Meta: ordering = ['sequence']
class MemberPlatformSettings(BasePlatformSettings): LOGIN_METHODS = ( ('password', _('Email/password combination')), ('SSO', _('Company SSO')), ) closed = models.BooleanField( default=False, help_text=_('Require login before accessing the platform')) login_methods = MultiSelectField(max_length=100, choices=LOGIN_METHODS, default=['password']) confirm_signup = models.BooleanField( default=False, help_text=_('Require verifying the user\'s email before signup')) email_domain = models.CharField( blank=True, null=True, help_text=_('Domain that all email should belong to'), max_length=256) require_consent = models.BooleanField( default=False, help_text=_('Require users to consent to cookies')) consent_link = models.CharField( default='/pages/terms-and-conditions', help_text=_('Link more information about the platforms policy'), max_length=255) background = models.ImageField( null=True, blank=True, upload_to='site_content/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) enable_segments = models.BooleanField( default=False, help_text=_('Enable segments for users e.g. department or job title.')) create_segments = models.BooleanField( default=False, help_text=_( 'Create new segments when a user logs in. ' 'Leave unchecked if only priorly specified ones should be used.')) anonymization_age = models.IntegerField( default=0, help_text= _("The number of days after which user data should be anonymised. 0 for no anonymisation" )) class Meta(object): verbose_name_plural = _('member platform settings') verbose_name = _('member platform settings')
class Category(TranslatableModel): slug = models.SlugField(_('slug'), max_length=100, unique=True) image = ImageField(_("image"), max_length=255, blank=True, null=True, upload_to='categories/', help_text=_("Category image")) video = models.FileField( _("video"), max_length=255, blank=True, null=True, validators=[ validate_video_file_size, FileMimetypeValidator( allowed_mimetypes=settings.VIDEO_FILE_ALLOWED_MIME_TYPES) ], help_text=_('This video will autoplay at the background. ' 'Allowed types are mp4, ogg, 3gp, avi, mov and webm. ' 'File should be smaller then 10MB.'), upload_to='banner_slides/') image_logo = ImageField(_("logo"), max_length=255, blank=True, null=True, upload_to='categories/logos/', help_text=_("Category Logo image")) translations = TranslatedFields(title=models.CharField(_("name"), max_length=255), description=models.TextField( _("description"))) class Meta(object): verbose_name = _("category") verbose_name_plural = _("categories") # ordering = ['title'] permissions = (('api_read_category', 'Can view categories through API'), ) def __str__(self): return self.title def save(self, **kwargs): if not self.slug: self.slug = slugify(self.title) super(Category, self).save(**kwargs) def get_absolute_url(self): return 'https://{}/projects/?category={}'.format( properties.tenant.domain_url, self.slug)
class NewsItem(AnonymizationMixin, PublishableModel): title = models.CharField(_("Title"), max_length=200) slug = models.SlugField(_("Slug")) # Contents main_image = ImageField( _("Main image"), help_text=_("Shows at the top of your post."), upload_to='blogs', blank=True, validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) language = models.CharField(_("language"), max_length=5, choices=lazy(get_languages, tuple)()) contents = PlaceholderField("blog_contents", plugins=[ 'TextPlugin', 'ImageTextPlugin', 'OEmbedPlugin', 'RawHtmlPlugin', 'PicturePlugin' ]) # This should not be necessary, but fixes deletion of some news items # See https://github.com/edoburu/django-fluent-contents/issues/19 contentitem_set = ContentItemRelation() allow_comments = models.BooleanField(_("Allow comments"), default=True) def __str__(self): return self.title def get_meta_description(self, **kwargs): request = kwargs.get('request') s = MLStripper() s.feed(mark_safe(render_placeholder(request, self.contents).html)) return truncatechars(s.get_data(), 250) class Meta(object): verbose_name = _("news item") verbose_name_plural = _("news items") permissions = ( ('api_read_newsitem', 'Can view news items through the API'), ('api_add_newsitem', 'Can add news items through the API'), ('api_change_newsitem', 'Can change news items through the API'), ('api_delete_newsitem', 'Can delete news items through the API'), )
class Quote(models.Model): block = models.ForeignKey('cms.QuotesContent', related_name='quotes') name = models.CharField(max_length=60) quote = models.TextField() image = ImageField( _("Image"), max_length=255, blank=True, null=True, upload_to='quote_images/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] )
class MailPlatformSettings(BasePlatformSettings): email_logo = models.ImageField( null=True, blank=True, upload_to='site_content/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) class Meta(object): verbose_name_plural = _('mail platform settings') verbose_name = _('mail platform settings')
class Logo(SortableMixin, models.Model): block = models.ForeignKey('cms.LogosContent', related_name='logos') image = ImageField( _("Image"), max_length=255, blank=True, null=True, upload_to='logo_images/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) link = models.CharField(max_length=100, blank=True, null=True) sequence = models.PositiveIntegerField(default=0, editable=False, db_index=True) class Meta: ordering = ['sequence']
class Organization(ValidatedModelMixin, AnonymizationMixin, models.Model): """ Organizations can run Projects. An organization has one or more members. """ name = models.CharField(_('name'), max_length=255) slug = models.SlugField(_('slug'), max_length=100) description = models.TextField(_('description'), default='', blank=True) created = CreationDateTimeField(_('created')) updated = ModificationDateTimeField(_('updated')) owner = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('owner'), null=True) website = models.URLField(_('website'), blank=True) logo = ImageField( _('logo'), blank=True, help_text=_('Partner Organization Logo'), max_length=255, null=True, upload_to='partner_organization_logos/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) required_fields = ['name', 'website'] def __str__(self): return self.name def save(self, *args, **kwargs): self.slug = slugify(self.name) super(Organization, self).save(*args, **kwargs) class Meta(object): ordering = ['name'] verbose_name = _("partner organization") verbose_name_plural = _("partner organizations")
class MediaWallpostPhoto(models.Model): mediawallpost = models.ForeignKey(MediaWallpost, related_name='photos', null=True, blank=True) results_page = models.BooleanField(default=True) photo = models.ImageField( upload_to='mediawallpostphotos', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) deleted = models.DateTimeField(_('deleted'), blank=True, null=True) ip_address = models.GenericIPAddressField(_('IP address'), blank=True, null=True, default=None) author = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('author'), related_name="%(class)s_wallpost_photo", blank=True, null=True) editor = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('editor'), blank=True, null=True, help_text=_("The last user to edit this wallpost photo.")) @property def owner(self): return self.author @property def parent(self): return self.mediawallpost.content_object def __str__(self): return "Wallpost photo #{}".format(self.id)
class Location(models.Model): name = models.CharField(_('name'), max_length=255) slug = models.SlugField(_('slug'), blank=False, null=True, max_length=255) position = GeopositionField(null=True) group = models.ForeignKey('geo.LocationGroup', verbose_name=_('location group'), null=True, blank=True) city = models.CharField(_('city'), blank=True, null=True, max_length=255) country = models.ForeignKey('geo.Country', blank=True, null=True) description = models.TextField(_('description'), blank=True) image = ImageField( _('image'), max_length=255, null=True, blank=True, upload_to='location_images/', help_text=_('Location picture'), validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) class Meta(GeoBaseModel.Meta): ordering = ['name'] verbose_name = _('office location') verbose_name_plural = _('office locations') def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Location, self).save() def __str__(self): return self.name
class ResultPage(TranslatableModel): image = models.ImageField( _('Header image'), blank=True, null=True, validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ] ) start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) content = PlaceholderField('content', plugins=[ 'ProjectMapBlockPlugin', 'QuotesBlockPlugin', 'ActivitiesBlockPlugin', 'ShareResultsBlockPlugin', 'StatsBlockPlugin', 'SupporterTotalBlockPlugin', ]) translations = TranslatedFields( title=models.CharField(_('Title'), max_length=40), slug=models.SlugField(_('Slug'), max_length=40), description=models.CharField(_('Description'), max_length=45, blank=True, null=True) ) class Meta: permissions = ( ('api_read_resultpage', 'Can view result pages through the API'), ('api_add_resultpage', 'Can add result pages through the API'), ('api_change_resultpage', 'Can change result pages through the API'), ('api_delete_resultpage', 'Can delete result pages through the API'), )
class Slide(PublishableModel): """ Slides for homepage """ class SlideStatus(DjangoChoices): published = ChoiceItem('published', label=_("Published")) draft = ChoiceItem('draft', label=_("Draft")) slug = models.SlugField(_("Slug")) language = models.CharField(_("language"), max_length=5, choices=lazy(get_languages, tuple)()) tab_text = models.CharField( _("Tab text"), max_length=100, help_text=_("This is shown on tabs beneath the banner.")) # Contents title = models.CharField(_("Title"), max_length=100, blank=True) body = models.TextField(_("Body text"), blank=True) image = ImageField( _("Image"), max_length=255, blank=True, null=True, upload_to='banner_slides/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) background_image = ImageField( _("Background image"), max_length=255, blank=True, null=True, upload_to='banner_slides/', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) video = models.FileField( _("Video"), max_length=255, blank=True, null=True, validators=[ validate_video_file_size, FileMimetypeValidator( allowed_mimetypes=settings.VIDEO_FILE_ALLOWED_MIME_TYPES) ], help_text=_('This video will autoplay at the background. ' 'Allowed types are mp4, ogg, 3gp, avi, mov and webm. ' 'File should be smaller then 10MB.'), upload_to='banner_slides/') video_url = models.URLField(_("Video url"), max_length=100, blank=True, default='') link_text = models.CharField( _("Link text"), max_length=400, blank=True, help_text=_("This is the text on the button inside the banner.")) link_url = models.CharField( _("Link url"), max_length=400, blank=True, help_text=_("This is the link for the button inside the banner.")) style = models.CharField(_("Style"), max_length=40, help_text=_("Styling class name"), default='default', blank=True) # Metadata sequence = models.IntegerField() @property def background_image_full_path(self): return "{0}{1}".format(settings.MEDIA_URL, str(self.background_image)) def __str__(self): return self.title class Meta(object): ordering = ('language', 'sequence')
class BlueBottleBaseUser(AbstractBaseUser, PermissionsMixin): """ Custom user model for BlueBottle. When extending the user model, the serializer should extend too. We provide a default base serializer in sync with the base user model The Django Meta attribute seems the best place for this configuration, so we have to add this. """ class Gender(DjangoChoices): male = ChoiceItem('male', label=_('Male')) female = ChoiceItem('female', label=_('Female')) class UserType(DjangoChoices): person = ChoiceItem('person', label=_('Person')) company = ChoiceItem('company', label=_('Company')) foundation = ChoiceItem('foundation', label=_('Foundation')) school = ChoiceItem('school', label=_('School')) group = ChoiceItem('group', label=_('Club / association')) email = models.EmailField(_('email address'), db_index=True, max_length=254, unique=True) username = models.CharField(_('username'), max_length=254, unique=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_( 'Designates whether the user can log into this admin site.')) is_active = models.BooleanField( _('active'), default=False, help_text=_( 'Designates whether this user should be treated as active. Unselect ' 'this instead of deleting accounts.')) disable_token = models.CharField(blank=True, max_length=32, null=True) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) updated = ModificationDateTimeField() last_seen = models.DateTimeField(_('Last Seen'), blank=True, null=True) deleted = models.DateTimeField(_('deleted'), blank=True, null=True) user_type = models.CharField(_('Member Type'), choices=UserType.choices, default=UserType.person, max_length=25) first_name = models.CharField(_('first name'), blank=True, max_length=100) last_name = models.CharField(_('last name'), blank=True, max_length=100) place = models.CharField(_('Location your at now'), blank=True, max_length=100) location = models.ForeignKey('geo.Location', blank=True, help_text=_('Location'), null=True, on_delete=models.SET_NULL) favourite_themes = models.ManyToManyField(ProjectTheme, blank=True) skills = models.ManyToManyField('tasks.Skill', blank=True) phone_number = models.CharField(_('phone number'), blank=True, max_length=50) gender = models.CharField(_('gender'), blank=True, choices=Gender.choices, max_length=6) birthdate = models.DateField(_('birthdate'), blank=True, null=True) about_me = models.TextField(_('about me'), blank=True, max_length=265) # TODO Use generate_picture_filename (or something) for upload_to picture = ImageField( _('picture'), blank=True, upload_to='profiles', validators=[ FileMimetypeValidator( allowed_mimetypes=settings.IMAGE_ALLOWED_MIME_TYPES, ), validate_file_infection ]) is_co_financer = models.BooleanField( _('Co-financer'), default=False, help_text=_( 'Donations by co-financers are shown in a separate list on the ' 'project page. These donation will always be visible.')) can_pledge = models.BooleanField( _('Can pledge'), default=False, help_text=_('User can create a pledge donation.')) # Use lazy for the choices and default, so that tenant properties # will be correctly loaded primary_language = models.CharField( _('primary language'), choices=lazy(get_language_choices, tuple)(), default=lazy(get_default_language, str)(), help_text=_('Language used for website and emails.'), max_length=5) share_time_knowledge = models.BooleanField(_('share time and knowledge'), default=False) share_money = models.BooleanField(_('share money'), default=False) newsletter = models.BooleanField(_('newsletter'), default=True, help_text=_('Subscribe to newsletter.')) campaign_notifications = models.BooleanField(_('Project Notifications'), default=True) website = models.URLField(_('website'), blank=True) facebook = models.CharField(_('facebook profile'), blank=True, max_length=50) twitter = models.CharField(_('twitter profile'), blank=True, max_length=15) skypename = models.CharField(_('skype profile'), blank=True, max_length=32) partner_organization = models.ForeignKey( 'organizations.Organization', blank=True, null=True, help_text=_('Users that are connected to a partner organisation ' 'will skip the organisation step in initiative create.'), related_name='partner_organization_members', verbose_name=_('Partner organisation')) is_anonymized = models.BooleanField(_('Is anonymized'), default=False) welcome_email_is_sent = models.BooleanField(_('Welcome email is sent'), default=False) USERNAME_FIELD = 'email' slug_field = 'username' objects = BlueBottleUserManager() class Meta(object): abstract = True verbose_name = _('member') verbose_name_plural = _('members') permissions = ( ('api_read_member', 'Can view members through the API'), ('api_read_full_member', 'Can view full members through the API'), ('api_add_member', 'Can add members through the API'), ('api_change_member', 'Can change members through the API'), ('api_delete_member', 'Can delete members through the API'), ('api_read_own_member', 'Can view own members through the API'), ('api_change_own_member', 'Can change own members through the API'), ('api_delete_own_member', 'Can delete own members through the API'), ) def update_deleted_timestamp(self): """ Automatically set or unset the deleted timestamp.""" if not self.is_active and self.deleted is None: self.deleted = timezone.now() elif self.is_active and self.deleted is not None: self.deleted = None def generate_username(self): """ Generate and set a username if it hasn't already been set. """ if not self.username: username = self.email original_username = username queryset = self.__class__.objects.all() if self.pk: queryset = queryset.exclude(pk=self.pk) # Increase the number while searching for the next valid slug # depending on the given slug, clean-up next_num = 2 while queryset.filter(username=username): username = original_username end = str(next_num) username = '******'.format(username, end) next_num += 1 # Finally set the generated username. self.username = username def clean(self): self.update_deleted_timestamp() self.generate_username() def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. """ full_name = u'{0} {1}'.format(self.first_name, self.last_name) return full_name.strip() def anonymize(self): self.is_active = False self.is_anonymized = True self.email = '{}[email protected]'.format( self.pk) # disabled emails need to be unique too self.username = '******'.format( self.pk) # disabled emails need to be unique too self.remote_id = '{}[email protected]'.format( self.pk) # disabled emails need to be unique too self.set_unusable_password() self.first_name = 'Deactivated' self.last_name = 'Member' self.user_name = '' self.picture = '' self.avatar = '' self.about_me = '' self.gender = '' self.birthdate = '1000-01-01' self.location = None self.website = '' self.facebook = '' self.twitter = '' self.skypename = '' self.partner_organization = None self.save() @property def full_name(self): return self.get_full_name() def get_short_name(self): """ The user is identified by their email address. """ return self.first_name def email_user(self, subject, message, from_email=None): """ Sends an email to this User with content type HTML. """ # It's possible to send multi-part text / HTML email by following these # instructions: https://docs.djangoproject.com/en/1.5/topics/email # /#sending-alternative-content-types msg = EmailMessage(subject, message, from_email, [self.email]) msg.content_subtype = 'html' # Main content is now text/html msg.send() def get_jwt_token(self): jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER payload = jwt_payload_handler(self) token = jwt_encode_handler(payload) return token def get_login_token(self): return login_token_generator.make_token(self) @property def short_name(self): return self.get_short_name() @cached_property def is_initiator(self): return bool(self.own_initiatives.count()) @cached_property def is_supporter(self): from bluebottle.funding.states import DonationStateMachine from bluebottle.funding.models import Donation return bool( self.contribution_set.instance_of(Donation).filter( status=DonationStateMachine.succeeded.value).count()) @cached_property def is_volunteer(self): from bluebottle.assignments.models import Applicant from bluebottle.events.models import Participant from bluebottle.activities.states import ActivityStateMachine return bool( self.contribution_set.instance_of(Applicant, Participant).filter( status=ActivityStateMachine.succeeded.value).count()) @cached_property def amount_donated(self): from bluebottle.funding.states import DonationStateMachine from bluebottle.funding.models import Donation from bluebottle.funding.utils import calculate_total donations = self.contribution_set.instance_of(Donation).filter( status=DonationStateMachine.succeeded.value) return calculate_total(donations) @cached_property def time_spent(self): from bluebottle.assignments.models import Applicant from bluebottle.events.models import Participant from bluebottle.activities.states import ActivityStateMachine contributions = self.contribution_set.instance_of(Applicant, Participant).\ filter(status=ActivityStateMachine.succeeded.value).all() return sum([c.time_spent for c in contributions]) @cached_property def subscribed(self): return self.campaign_notifications def reset_disable_token(self): # Generates a random UUID and converts it to a 32-character # hexidecimal string token = uuid.uuid4().hex self.disable_token = token self.save() def get_disable_token(self): if not self.disable_token: self.reset_disable_token() return self.disable_token def save(self, force_insert=False, force_update=False, using=None, update_fields=None): self.generate_username() super(BlueBottleBaseUser, self).save(force_insert, force_update, using, update_fields) def __getattr__(self, name): # Magically get extra fields if name.startswith('extra_'): name = name.replace('extra_', '') return self.extra.get(field__name=name).value return super(BlueBottleBaseUser, self).__getattribute__(name)