def test_default_choices_not_frozen(self): """ Ensure the deconstructed representation of the field does not contain kwargs if they match the default. Don't want to bloat everyone's migration files. """ field = TimeZoneField() name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs) self.assertNotIn('max_length', kwargs)
def test_from_db_value(self): """ Verify that the field can handle data coming back as bytes from the db. """ field = TimeZoneField() # django 1.11 signuature value = field.from_db_value(b'UTC', None, None, None) self.assertEqual(pytz.UTC, value) # django 2.0+ signuature value = field.from_db_value(b'UTC', None, None) self.assertEqual(pytz.UTC, value)
def test_specifying_defaults_not_frozen(self): """ If someone's matched the default values with their kwarg args, we shouldn't bothering freezing those. """ field = TimeZoneField(max_length=63) name, path, args, kwargs = field.deconstruct() self.assertNotIn('max_length', kwargs) choices = [(pytz.timezone(tz), tz) for tz in pytz.common_timezones] field = TimeZoneField(choices=choices) name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs) choices = [(tz, tz) for tz in pytz.common_timezones] field = TimeZoneField(choices=choices) name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs)
class Event(models.Model): objects = EventManager() short = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=128) receivername = models.CharField(max_length=128, blank=True, null=False, verbose_name='Receiver Name') targetamount = models.DecimalField(decimal_places=2, max_digits=20, validators=[positive, nonzero], verbose_name='Target Amount') minimumdonation = models.DecimalField( decimal_places=2, max_digits=20, validators=[positive, nonzero], verbose_name='Minimum Donation', help_text='Enforces a minimum donation amount on the donate page.', default=decimal.Decimal('1.00')) usepaypalsandbox = models.BooleanField(default=False, verbose_name='Use Paypal Sandbox') paypalemail = models.EmailField(max_length=128, null=False, blank=False, verbose_name='Receiver Paypal') paypalcurrency = models.CharField(max_length=8, null=False, blank=False, default=_currencyChoices[0][0], choices=_currencyChoices, verbose_name='Currency') donationemailtemplate = models.ForeignKey( post_office.models.EmailTemplate, verbose_name='Donation Email Template', default=None, null=True, blank=True, on_delete=models.PROTECT, related_name='event_donation_templates') pendingdonationemailtemplate = models.ForeignKey( post_office.models.EmailTemplate, verbose_name='Pending Donation Email Template', default=None, null=True, blank=True, on_delete=models.PROTECT, related_name='event_pending_donation_templates') donationemailsender = models.EmailField( max_length=128, null=True, blank=True, verbose_name='Donation Email Sender') scheduleid = models.CharField(max_length=128, unique=True, null=True, blank=True, verbose_name='Schedule ID') scheduletimezone = models.CharField(max_length=64, blank=True, choices=_timezoneChoices, default='US/Eastern', verbose_name='Schedule Timezone') scheduledatetimefield = models.CharField(max_length=128, blank=True, verbose_name='Schedule Datetime') schedulegamefield = models.CharField(max_length=128, blank=True, verbose_name='Schdule Game') schedulerunnersfield = models.CharField(max_length=128, blank=True, verbose_name='Schedule Runners') scheduleestimatefield = models.CharField(max_length=128, blank=True, verbose_name='Schedule Estimate') schedulesetupfield = models.CharField(max_length=128, blank=True, verbose_name='Schedule Setup') schedulecommentatorsfield = models.CharField( max_length=128, blank=True, verbose_name='Schedule Commentators') schedulecommentsfield = models.CharField(max_length=128, blank=True, verbose_name='Schedule Comments') date = models.DateField() timezone = TimeZoneField(default='US/Eastern') locked = models.BooleanField( default=False, help_text= 'Requires special permission to edit this event or anything associated with it' ) # Fields related to prize management prizecoordinator = models.ForeignKey( User, default=None, null=True, blank=True, verbose_name='Prize Coordinator', help_text= 'The person responsible for managing prize acceptance/distribution') allowed_prize_countries = models.ManyToManyField( 'Country', blank=True, verbose_name="Allowed Prize Countries", help_text= "List of countries whose residents are allowed to receive prizes (leave blank to allow all countries)" ) disallowed_prize_regions = models.ManyToManyField( 'CountryRegion', blank=True, verbose_name='Disallowed Regions', help_text= 'A blacklist of regions within allowed countries that are not allowed for drawings (e.g. Quebec in Canada)' ) prize_accept_deadline_delta = models.IntegerField( default=14, null=False, blank=False, verbose_name='Prize Accept Deadline Delta', help_text= 'The number of days a winner will be given to accept a prize before it is re-rolled.', validators=[positive, nonzero]) prizecontributoremailtemplate = models.ForeignKey( post_office.models.EmailTemplate, default=None, null=True, blank=True, verbose_name='Prize Contributor Accept/Deny Email Template', help_text= "Email template to use when responding to prize contributor's submission requests", related_name='event_prizecontributortemplates') prizewinneremailtemplate = models.ForeignKey( post_office.models.EmailTemplate, default=None, null=True, blank=True, verbose_name='Prize Winner Email Template', help_text="Email template to use when someone wins a prize.", related_name='event_prizewinnertemplates') prizewinneracceptemailtemplate = models.ForeignKey( post_office.models.EmailTemplate, default=None, null=True, blank=True, verbose_name='Prize Accepted Email Template', help_text= "Email template to use when someone accepts a prize (and thus it needs to be shipped).", related_name='event_prizewinneraccepttemplates') prizeshippedemailtemplate = models.ForeignKey( post_office.models.EmailTemplate, default=None, null=True, blank=True, verbose_name='Prize Shipped Email Template', help_text= "Email template to use when the aprize has been shipped to its recipient).", related_name='event_prizeshippedtemplates') def __unicode__(self): return self.name def natural_key(self): return (self.short, ) def clean(self): if self.id and self.id < 1: raise ValidationError('Event ID must be positive and non-zero') if not re.match('^\w+$', self.short): raise ValidationError('Event short name must be a url-safe string') if not self.scheduleid: self.scheduleid = None if self.donationemailtemplate != None or self.pendingdonationemailtemplate != None: if not self.donationemailsender: raise ValidationError( 'Must specify a donation email sender if automailing is used' ) def start_push_notification(self, request): from django.core.urlresolvers import reverse approval_force = False try: credentials = CredentialsModel.objects.get( id=request.user).credentials if credentials: if not credentials.refresh_token: approval_force = True raise CredentialsModel.DoesNotExist elif credentials.access_token_expired: import httplib2 credentials.refresh(httplib2.Http()) except CredentialsModel.DoesNotExist: from django.conf import settings from django.http import HttpResponseRedirect FlowModel.objects.filter(id=request.user).delete() kwargs = {} if approval_force: kwargs['approval_prompt'] = 'force' defaultflow = OAuth2WebServerFlow( client_id=settings.GOOGLE_CLIENT_ID, client_secret=settings.GOOGLE_CLIENT_SECRET, scope='https://www.googleapis.com/auth/drive.metadata.readonly', redirect_uri=request.build_absolute_uri( reverse('admin:google_flow')).replace( '/cutler5:', '/cutler5.example.com:'), access_type='offline', **kwargs) flow = FlowModel(id=request.user, flow=defaultflow) flow.save() url = flow.flow.step1_get_authorize_url() return HttpResponseRedirect(url) from apiclient.discovery import build import httplib2 import uuid import time drive = build('drive', 'v2', credentials.authorize(httplib2.Http())) body = { 'kind': 'api#channel', 'resourceId': self.scheduleid, 'id': unicode(uuid.uuid4()), 'token': u'%s:%s' % (self.id, unicode(request.user)), 'type': 'web_hook', 'address': request.build_absolute_uri( reverse('tracker.views.refresh_schedule')), 'expiration': int(time.time() + 24 * 60 * 60) * 1000 # approx one day } try: drive.files().watch(fileId=self.scheduleid, body=body).execute() except Exception as e: from django.contrib import messages messages.error(request, u'Could not start push notification: %s' % e) return False return True class Meta: app_label = 'tracker' get_latest_by = 'date' permissions = (('can_edit_locked_events', 'Can edit locked events'), ) ordering = ('date', )
class MemberProfile(index.Indexed, ClusterableModel): """ Contains additional comses.net information, possibly linked to a CoMSES Member / site account """ user = models.OneToOneField(User, null=True, on_delete=models.SET_NULL, related_name='member_profile') # FIXME: add location field eventually, with postgis # location = LocationField(based_fields=['city'], zoom=7) timezone = TimeZoneField(blank=True) affiliations = JSONField( default=list, help_text=_("JSON-LD list of affiliated institutions")) bio = MarkdownField(max_length=2048, help_text=_('Brief bio')) degrees = ArrayField(models.CharField(max_length=255), blank=True, default=list) institution = models.ForeignKey(Institution, null=True, on_delete=models.SET_NULL) tags = ClusterTaggableManager(through=MemberProfileTag, blank=True) personal_url = models.URLField(blank=True) picture = models.ForeignKey(Image, null=True, help_text=_('Profile picture'), on_delete=models.SET_NULL) professional_url = models.URLField(blank=True) research_interests = MarkdownField(max_length=2048) objects = MemberProfileQuerySet.as_manager() panels = [ FieldPanel('bio', widget=forms.Textarea), FieldPanel('research_interests', widget=forms.Textarea), FieldPanel('personal_url'), FieldPanel('professional_url'), FieldPanel('institution'), ImageChooserPanel('picture'), FieldPanel('tags'), ] search_fields = [ index.SearchField('bio', partial_match=True, boost=5), index.SearchField('research_interests', partial_match=True, boost=5), index.FilterField('is_active'), index.FilterField('username'), index.SearchField('degrees', partial_match=True), index.SearchField('name', partial_match=True, boost=5), index.RelatedFields('institution', [ index.SearchField('name', partial_match=True), ]), index.RelatedFields('tags', [ index.SearchField('name', partial_match=True), ]), index.RelatedFields('user', [ index.SearchField('first_name', partial_match=True), index.SearchField('last_name', partial_match=True, boost=3), index.SearchField('email', partial_match=True, boost=3), index.SearchField('username', partial_match=True, boost=5), ]), ] """ Returns the ORCID profile URL associated with this member profile if it exists, or None """ @property def orcid_url(self): return self.get_social_account_profile_url('orcid') @property def avatar_url(self): if self.picture: return self.picture.get_rendition('fill-150x150').url return None """ Returns the github profile URL associated with this member profile if it exists, or None """ # Proxies to related user object @property def date_joined(self): return self.user.date_joined @property def email(self): return self.user.email @property def username(self): return self.user.username @property def is_active(self): return self.user.is_active # Urls @property def github_url(self): return self.get_social_account_profile_url('github') def get_social_account_profile_url(self, provider_name): social_acct = self.get_social_account(provider_name) if social_acct: return social_acct.get_profile_url() return None def get_social_account(self, provider_name): return self.user.socialaccount_set.filter( provider=provider_name).first() @property def institution_url(self): return self.institution.url if self.institution else '' @property def profile_url(self): return self.get_absolute_url() def get_absolute_url(self): return reverse('home:profile-detail', kwargs={'pk': self.user.pk}) def get_edit_url(self): return reverse('home:profile-edit', kwargs={'user__pk': self.user.pk}) @classmethod def get_list_url(cls): return reverse('home:profile-list') # Other @property def institution_name(self): return self.institution.name if self.institution else '' @property def submitter(self): return self.user @property def is_reviewer(self): return self.user.groups.filter( name=ComsesGroups.REVIEWER.value).exists() @property def name(self): return self.user.get_full_name() or self.user.username @property def full_member(self): return self.user.groups.filter( name=ComsesGroups.FULL_MEMBER.value).exists() @full_member.setter def full_member(self, value): group = Group.objects.get(name=ComsesGroups.FULL_MEMBER.value) if value: self.user.groups.add(group) else: self.user.groups.remove(group) def is_messageable(self, user): return user.is_authenticated and user != self.user def __str__(self): return str(self.user)
class News(TendenciBaseModel): CONTRIBUTOR_AUTHOR = 1 CONTRIBUTOR_PUBLISHER = 2 CONTRIBUTOR_CHOICES = ((CONTRIBUTOR_AUTHOR, _('Author')), (CONTRIBUTOR_PUBLISHER, _('Publisher'))) guid = models.CharField(max_length=40) slug = SlugField(_('URL Path'), unique=True) timezone = TimeZoneField(verbose_name=_('Time Zone'), default='US/Central', choices=get_timezone_choices(), max_length=100) headline = models.CharField(max_length=200, blank=True) summary = models.TextField(blank=True) body = tinymce_models.HTMLField() source = models.CharField(max_length=300, blank=True) first_name = models.CharField(_('First Name'), max_length=100, blank=True) last_name = models.CharField(_('Last Name'), max_length=100, blank=True) contributor_type = models.IntegerField(choices=CONTRIBUTOR_CHOICES, default=CONTRIBUTOR_AUTHOR) phone = models.CharField(max_length=50, blank=True) fax = models.CharField(max_length=50, blank=True) email = models.CharField(max_length=120, blank=True) website = models.CharField(max_length=300, blank=True) thumbnail = models.ForeignKey(NewsImage, default=None, null=True, on_delete=models.SET_NULL, help_text=_('The thumbnail image can be used on your homepage or sidebar if it is setup in your theme. The thumbnail image will not display on the news page.')) release_dt = models.DateTimeField(_('Release Date/Time'), null=True, blank=True) # used for better performance when retrieving a list of released news release_dt_local = models.DateTimeField(null=True, blank=True) syndicate = models.BooleanField(_('Include in RSS feed'), default=True) design_notes = models.TextField(_('Design Notes'), blank=True) groups = models.ManyToManyField(Group, default=get_default_group, related_name='group_news') tags = TagField(blank=True) #for podcast feeds enclosure_url = models.CharField(_('Enclosure URL'), max_length=500, blank=True) # for podcast feeds enclosure_type = models.CharField(_('Enclosure Type'),max_length=120, blank=True) # for podcast feeds enclosure_length = models.IntegerField(_('Enclosure Length'), default=0) # for podcast feeds use_auto_timestamp = models.BooleanField(_('Auto Timestamp'), default=False) # html-meta tags meta = models.OneToOneField(MetaTags, null=True, on_delete=models.SET_NULL) categories = GenericRelation(CategoryItem, object_id_field="object_id", content_type_field="content_type") perms = GenericRelation(ObjectPermission, object_id_field="object_id", content_type_field="content_type") objects = NewsManager() class Meta: # permissions = (("view_news",_("Can view news")),) verbose_name_plural = _("News") app_label = 'news' def get_meta(self, name): """ This method is standard across all models that are related to the Meta model. Used to generate dynamic meta information niche to this model. """ return NewsMeta().get_meta(self, name) def get_absolute_url(self): return reverse('news.detail', args=[self.slug]) def __str__(self): return self.headline def save(self, *args, **kwargs): if not self.id: self.guid = str(uuid.uuid4()) self.assign_release_dt_local() photo_upload = kwargs.pop('photo', None) super(News, self).save(*args, **kwargs) if photo_upload and self.pk: image = NewsImage( object_id=self.pk, creator=self.creator, creator_username=self.creator_username, owner=self.owner, owner_username=self.owner_username ) photo_upload.file.seek(0) image.file.save(photo_upload.name, photo_upload) # save file row image.save() # save image row if self.thumbnail: self.thumbnail.delete() # delete image and file row self.thumbnail = image # set image self.save() if self.thumbnail: if self.is_public(): set_s3_file_permission(self.thumbnail.file, public=True) else: set_s3_file_permission(self.thumbnail.file, public=False) @property def category_set(self): items = {} for cat in self.categories.select_related('category', 'parent'): if cat.category: items["category"] = cat.category elif cat.parent: items["sub_category"] = cat.parent return items def is_public(self): return all([self.allow_anonymous_view, self.status, self.status_detail in ['active']]) @property def is_released(self): return self.release_dt_local <= datetime.now() @property def has_google_author(self): return self.contributor_type == self.CONTRIBUTOR_AUTHOR @property def has_google_publisher(self): return self.contributor_type == self.CONTRIBUTOR_PUBLISHER def assign_release_dt_local(self): """ convert release_dt to the corresponding local time example: if release_dt: 2014-05-09 03:30:00 timezone: US/Pacific settings.TIME_ZONE: US/Central then the corresponding release_dt_local will be: 2014-05-09 05:30:00 """ now = datetime.now() now_with_tz = adjust_datetime_to_timezone(now, settings.TIME_ZONE) if self.timezone and self.release_dt and self.timezone.zone != settings.TIME_ZONE: time_diff = adjust_datetime_to_timezone(now, self.timezone) - now_with_tz self.release_dt_local = self.release_dt + time_diff else: self.release_dt_local = self.release_dt
class Article(TendenciBaseModel): CONTRIBUTOR_AUTHOR = 1 CONTRIBUTOR_PUBLISHER = 2 CONTRIBUTOR_CHOICES = ((CONTRIBUTOR_AUTHOR, _('Author')), (CONTRIBUTOR_PUBLISHER, _('Publisher'))) guid = models.CharField(max_length=40) slug = SlugField(_('URL Path'), unique=True) timezone = TimeZoneField(verbose_name=_('Time Zone'), default='US/Central', choices=get_timezone_choices(), max_length=100) headline = models.CharField(max_length=200, blank=True) summary = models.TextField(blank=True) body = tinymce_models.HTMLField() source = models.CharField(max_length=300, blank=True) first_name = models.CharField(_('First Name'), max_length=100, blank=True) last_name = models.CharField(_('Last Name'), max_length=100, blank=True) contributor_type = models.IntegerField(choices=CONTRIBUTOR_CHOICES, default=CONTRIBUTOR_AUTHOR) phone = models.CharField(max_length=50, blank=True) fax = models.CharField(max_length=50, blank=True) email = models.CharField(max_length=120, blank=True) website = models.CharField(max_length=300, blank=True) thumbnail = models.ForeignKey( File, null=True, on_delete=models.SET_NULL, help_text=_( 'The thumbnail image can be used on your homepage ' + 'or sidebar if it is setup in your theme. The thumbnail image ' + 'will not display on the news page.')) release_dt = models.DateTimeField(_('Release Date/Time'), null=True, blank=True) # used for better performance when retrieving a list of released articles release_dt_local = models.DateTimeField(null=True, blank=True) syndicate = models.BooleanField(_('Include in RSS feed'), default=True) featured = models.BooleanField(default=False) design_notes = models.TextField(_('Design Notes'), blank=True) group = models.ForeignKey(Group, null=True, default=get_default_group, on_delete=models.SET_NULL) tags = TagField(blank=True) # for podcast feeds enclosure_url = models.CharField(_('Enclosure URL'), max_length=500, blank=True) enclosure_type = models.CharField(_('Enclosure Type'), max_length=120, blank=True) enclosure_length = models.IntegerField(_('Enclosure Length'), default=0) not_official_content = models.BooleanField(_('Official Content'), blank=True, default=True) # html-meta tags meta = models.OneToOneField(MetaTags, null=True, on_delete=models.SET_NULL) categories = GenericRelation(CategoryItem, object_id_field="object_id", content_type_field="content_type") perms = GenericRelation(ObjectPermission, object_id_field="object_id", content_type_field="content_type") objects = ArticleManager() class Meta: permissions = (("view_article", _("Can view article")), ) verbose_name = _("Article") verbose_name_plural = _("Articles") app_label = 'articles' def get_meta(self, name): """ This method is standard across all models that are related to the Meta model. Used to generate dynamic methods coupled to this instance. """ return ArticleMeta().get_meta(self, name) def get_absolute_url(self): return reverse('article', args=[self.slug]) def get_version_url(self, hash): return reverse('article.version', args=[hash]) def __str__(self): return self.headline def get_thumbnail_url(self): if not self.thumbnail: return u'' return reverse('file', args=[self.thumbnail.pk]) def save(self, *args, **kwargs): if not self.id: self.guid = str(uuid.uuid4()) self.assign_release_dt_local() super(Article, self).save(*args, **kwargs) def assign_release_dt_local(self): """ convert release_dt to the corresponding local time example: if release_dt: 2014-05-09 03:30:00 timezone: US/Pacific settings.TIME_ZONE: US/Central then the corresponding release_dt_local will be: 2014-05-09 05:30:00 """ now = datetime.now() now_with_tz = adjust_datetime_to_timezone(now, settings.TIME_ZONE) if self.timezone and self.release_dt and self.timezone.zone != settings.TIME_ZONE: time_diff = adjust_datetime_to_timezone( now, self.timezone) - now_with_tz self.release_dt_local = self.release_dt + time_diff else: self.release_dt_local = self.release_dt def age(self): return datetime.now() - self.create_dt @property def category_set(self): items = {} for cat in self.categories.select_related('category', 'parent'): if cat.category: items["category"] = cat.category elif cat.parent: items["sub_category"] = cat.parent return items @property def has_google_author(self): return self.contributor_type == self.CONTRIBUTOR_AUTHOR @property def has_google_publisher(self): return self.contributor_type == self.CONTRIBUTOR_PUBLISHER
class AdminModel(models.Model): ''' An abstract model that adds some admin fields and overrides the save method to ensure that on every save these fields are updated (who and when) ''' # Simple history and administrative fields created_by = models.ForeignKey(User, verbose_name='Created By', related_name='%(class)ss_created', editable=False, null=True, on_delete=models.SET_NULL) created_on = models.DateTimeField('Time of Creation', editable=False, null=True) created_on_tz = TimeZoneField('Time of Creation, Timezone', default=settings.TIME_ZONE, editable=False) last_edited_by = models.ForeignKey(User, verbose_name='Last Edited By', related_name='%(class)ss_last_edited', editable=False, null=True, on_delete=models.SET_NULL) last_edited_on = models.DateTimeField('Time of Last Edit', editable=False, null=True) last_edited_on_tz = TimeZoneField('Time of Last Edit, Timezone', default=settings.TIME_ZONE, editable=False) def update_admin_fields(self): ''' Update the CoGs admin fields on an object (whenever it is saved). ''' now = timezone.now() usr = CuserMiddleware.get_user() if hasattr(self, "last_edited_by"): self.last_edited_by = usr if hasattr(self, "last_edited_on"): self.last_edited_on = now if hasattr(self, "last_edited_on_tz"): self.last_edited_on_tz = str(get_current_timezone()) # We infer that if the object has pk it was being edited and if it has none it was being created if self.pk is None: if hasattr(self, "created_by"): self.created_by = usr if hasattr(self, "created_on"): self.created_on = now if hasattr(self, "created_on_tz"): self.created_on_tz = str(get_current_timezone()) def save(self, *args, **kwargs): self.update_admin_fields() super().save(*args, **kwargs) class Meta: get_latest_by = "created_on" abstract = True
class Airport(models.Model): title = models.CharField( verbose_name='Long Name of Airport', max_length=50) timezone = TimeZoneField(default='US/Eastern') abrev = models.CharField( verbose_name='Airport Abreviation Code', max_length=4, primary_key=True) latitude = models.FloatField( validators=[MinValueValidator(-90), MaxValueValidator(90)]) longitude = models.FloatField( validators=[MinValueValidator(-180), MaxValueValidator(180)]) sw_airport = models.BooleanField(verbose_name='Southwest Airport') country = models.CharField(max_length=20, blank=True) state = models.CharField(max_length=20, blank=True) objects = AirportManager() sw_airport.admin_order_field = 'title' def __str__(self): # Return the title and abrev as the default string return self.title + " - " + self.abrev def _get_sub_loc(self, key): # Here we use geolocator to get the proper key geolocator = Nominatim() location = geolocator.reverse("{:f}, {:f}".format( self.latitude, self.longitude), timeout=10) # Lots and lots of error checking...looking for error from Geolocator # and for missing fields for international or other addresses if 'error' in location.raw: # Got an error back from geolocator raise ValidationError(_( "Geolocator error: %(error)s - Check you have the right Lat/Long or that you have connection"), params=location.raw, code='geolocator') # Got a response...but we may be missing keys...looking here try: return location.raw['address'][key] except KeyError as err: if err == 'address': raise ValidationError( _('Got a response from Geolocator, but had no address'), code='no_address') elif err == key: raise ValidationError(_('Got a response from Geolocator, had an address, but didnt have key: %(key)s'), params={ 'key': err}, code='no_{}'.format(err)) else: raise ValidationError(_('Got a response from Geolocator, had an address,KEY_ERROR of some kind %(raw)s'), params={ 'raw': location.raw}, code='some_key') except: raise ValidationError( _('Geolocator - NO CLUE WHAT WENT WRONG'), code='no_clue') def get_tz_obj(self): if isinstance(self.timezone, string_types): return pytz.timezone(self.timezone) else: return self.timezone def get_country_code(self): return self._get_sub_loc('country_code') def get_state(self): return self._get_sub_loc('state') def add_loc_fields(self): if (self.country is None) or (self.country == ''): self.country = self.get_country_code() if ((self.state is None) or (self.state == '')) and self.country == 'us': self.state = self.get_state()
class RRule(models.Model): """ Model that will hold rrule details and generate recurrences to be handled by the supplied handler """ # Params used to generate the rrule rrule_params = JSONField() # Any meta data associated with the object that created this rule meta_data = JSONField(default=dict) # The timezone all dates should be converted to time_zone = TimeZoneField(default='UTC') # The last occurrence date that was handled last_occurrence = models.DateTimeField(null=True, default=None) # The next occurrence date that should be handled next_occurrence = models.DateTimeField(null=True, default=None) # A python path to the handler class used to handle when a recurrence occurs for this rrule # The configuration class must extend ambition_utils.rrule.handler.OccurrenceHandler occurrence_handler_path = models.CharField(max_length=500, blank=False, null=False) # Custom object manager objects = RRuleManager() def get_occurrence_handler_class_instance(self): """ Gets an instance of the occurrence handler class associated with this rrule :rtype: ambition_utils.rrule.handler.OccurrenceHandler :return: The instance """ return import_string(self.occurrence_handler_path)() def get_rrule(self): """ Builds the rrule object by restoring all the params. The dtstart param will be converted to local time if it is set. :rtype: rrule """ params = copy.deepcopy(self.rrule_params) # Convert next scheduled from utc back to time zone if params.get('dtstart') and not hasattr(params.get('dtstart'), 'date'): params['dtstart'] = parser.parse(params['dtstart']) # Convert until date from utc back to time zone if params.get('until') and not hasattr(params.get('until'), 'date'): params['until'] = parser.parse(params['until']) # Always cache params['cache'] = True # Return the rrule return rrule(**params) def get_next_occurrence(self, last_occurrence=None, force=False): """ Builds the rrule and returns the next date in the series or None of it is the end of the series :param last_occurrence: The last occurrence that was generated :param force: If the next occurrence is none, force the rrule to generate another :rtype: rrule or None """ # Get the last occurrence last_occurrence = last_occurrence or self.last_occurrence or datetime.utcnow() # Get the rule rule = self.get_rrule() # Convert to local time zone for getting next occurrence, otherwise time zones ahead of utc will return the same last_occurrence = fleming.convert_to_tz(last_occurrence, self.time_zone, return_naive=True) # Generate the next occurrence next_occurrence = rule.after(last_occurrence) # If next occurrence is none and force is true, force the rrule to generate another date if next_occurrence is None and force: # Keep a reference to the original rrule_params original_rrule_params = {} original_rrule_params.update(self.rrule_params) # Remove any limiting params self.rrule_params.pop('count', None) self.rrule_params.pop('until', None) # Refetch the rule rule = self.get_rrule() # Generate the next occurrence next_occurrence = rule.after(last_occurrence) # Restore the rrule params self.rrule_params = original_rrule_params # If there is a next occurrence, convert to utc if next_occurrence: next_occurrence = self.convert_to_utc(next_occurrence) # Return the next occurrence return next_occurrence def update_next_occurrence(self, save=True): """ Sets the next_occurrence property to the next time in the series and sets the last_occurrence property to the previous value of next_occurrence. If the save option is True, the model will be saved. The save flag is typically set to False when wanting to bulk update records after updating the values of many models. :param save: Flag to save the model after updating the schedule. :type save: bool """ if not self.next_occurrence: return None # Only handle if the current date is >= next occurrence if datetime.utcnow() < self.next_occurrence: return False self.last_occurrence = self.next_occurrence self.next_occurrence = self.get_next_occurrence(self.last_occurrence) # Only save if the flag is true if save: self.save(update_fields=['last_occurrence', 'next_occurrence']) def convert_to_utc(self, dt): """ Treats the datetime object as being in the timezone of self.timezone and then converts it to utc timezone. :type dt: datetime """ # Add timezone info dt = fleming.attach_tz_if_none(dt, self.time_zone) # Convert to utc dt = fleming.convert_to_tz(dt, pytz.utc, return_naive=True) return dt def refresh_next_occurrence(self, current_time=None): """ Sets the next occurrence date based on the current rrule param definition. The date will be after the specified current_time or utcnow. :param current_time: Optional datetime object to compute the next time from """ # Get the current time or go off the specified current time current_time = current_time or datetime.utcnow() # Next occurrence is in utc here next_occurrence = self.get_next_occurrence(last_occurrence=current_time) # Check if the start time is different but still greater than now if next_occurrence != self.next_occurrence and next_occurrence > datetime.utcnow(): self.next_occurrence = next_occurrence def save(self, *args, **kwargs): """ Saves the rrule model to the database. If this is a new object, the first next_scheduled time is determined and set. The `dtstart` and `until` objects will be safely encoded as strings if they are datetime objects. """ # Check if this is a new rrule object if self.pk is None: # Convert next scheduled from utc back to time zone if self.rrule_params.get('dtstart') and not hasattr(self.rrule_params.get('dtstart'), 'date'): self.rrule_params['dtstart'] = parser.parse(self.rrule_params['dtstart']) # Convert until date from utc back to time zone if self.rrule_params.get('until') and not hasattr(self.rrule_params.get('until'), 'date'): self.rrule_params['until'] = parser.parse(self.rrule_params['until']) # Get the first scheduled time according to the rrule (this converts from utc back to local time) self.next_occurrence = self.get_rrule()[0] # Convert back to utc before saving self.next_occurrence = self.convert_to_utc(self.next_occurrence) # Serialize the datetime objects if they exist if self.rrule_params.get('dtstart') and hasattr(self.rrule_params.get('dtstart'), 'date'): self.rrule_params['dtstart'] = self.rrule_params['dtstart'].strftime('%Y-%m-%d %H:%M:%S') if self.rrule_params.get('until') and hasattr(self.rrule_params.get('until'), 'date'): self.rrule_params['until'] = self.rrule_params['until'].strftime('%Y-%m-%d %H:%M:%S') # Call the parent save method super().save(*args, **kwargs)
def test_invalid_choices_display(self): self.assertRaises(ValueError, lambda: TimeZoneField(choices_display='invalid'))
def test_deconstruct(self): for org_field in self.test_fields: name, path, args, kwargs = org_field.deconstruct() new_field = TimeZoneField(*args, **kwargs) self.assertEqual(org_field.max_length, new_field.max_length) self.assertEqual(org_field.choices, new_field.choices)
class TimeZoneFieldDeconstructTestCase(TestCase): test_fields = ( TimeZoneField(), TimeZoneField(default='UTC'), TimeZoneField(max_length=42), TimeZoneField(choices=[ (pytz.timezone('US/Pacific'), 'US/Pacific'), (pytz.timezone('US/Eastern'), 'US/Eastern'), ]), TimeZoneField(choices=[ (pytz.timezone(b'US/Pacific'), b'US/Pacific'), (pytz.timezone(b'US/Eastern'), b'US/Eastern'), ]), TimeZoneField(choices=[ ('US/Pacific', 'US/Pacific'), ('US/Eastern', 'US/Eastern'), ]), TimeZoneField(choices=[ (b'US/Pacific', b'US/Pacific'), (b'US/Eastern', b'US/Eastern'), ]), ) def test_deconstruct(self): for org_field in self.test_fields: name, path, args, kwargs = org_field.deconstruct() new_field = TimeZoneField(*args, **kwargs) self.assertEqual(org_field.max_length, new_field.max_length) self.assertEqual(org_field.choices, new_field.choices) def test_full_serialization(self): # ensure the values passed to kwarg arguments can be serialized # the recommended 'deconstruct' testing by django docs doesn't cut it # https://docs.djangoproject.com/en/1.7/howto/custom-model-fields/#field-deconstruction # replicates https://github.com/mfogel/django-timezone-field/issues/12 for field in self.test_fields: # ensuring the following call doesn't throw an error MigrationWriter.serialize(field) def test_from_db_value(self): """ Verify that the field can handle data coming back as bytes from the db. """ field = TimeZoneField() # django 1.11 signuature value = field.from_db_value(b'UTC', None, None, None) self.assertEqual(pytz.UTC, value) # django 2.0+ signuature value = field.from_db_value(b'UTC', None, None) self.assertEqual(pytz.UTC, value) def test_default_kwargs_not_frozen(self): """ Ensure the deconstructed representation of the field does not contain kwargs if they match the default. Don't want to bloat everyone's migration files. """ field = TimeZoneField() name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs) self.assertNotIn('max_length', kwargs) def test_specifying_defaults_not_frozen(self): """ If someone's matched the default values with their kwarg args, we shouldn't bothering freezing those. """ field = TimeZoneField(max_length=63) name, path, args, kwargs = field.deconstruct() self.assertNotIn('max_length', kwargs) choices = [(pytz.timezone(tz), tz.replace('_', ' ')) for tz in pytz.common_timezones] field = TimeZoneField(choices=choices) name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs) choices = [(tz, tz.replace('_', ' ')) for tz in pytz.common_timezones] field = TimeZoneField(choices=choices) name, path, args, kwargs = field.deconstruct() self.assertNotIn('choices', kwargs)
def createField(): TimeZoneField('a verbose name', 'a name', True, 42)
def test_some_positional_args_ok(self): TimeZoneField('a verbose name', 'a name', True)
class TimerUser(AbstractUser): timezone = TimeZoneField(default='Canada/Mountain')