class Contact(ContactModel): @ChoiceEnum.labels(str.capitalize) class Type(ChoiceEnum): manager = 0 contact = 1 ugettext_noop("Manager") ugettext_noop("Contact") center = models.ForeignKey( Center, related_name='contacts', on_delete=models.CASCADE ) type = EnumField(Type, db_index=True) def __str__(self): return "{} ({})".format(self.get_full_name(), Contact.Type(self.type))
class EventWish(models.Model): applicant = models.ForeignKey(Applicant, on_delete=models.CASCADE) event = models.ForeignKey(Event, on_delete=models.CASCADE) status = EnumField( ApplicantStatusTypes, db_index=True, blank=True, default=ApplicantStatusTypes.incomplete.value, ) # Priority defined by the candidate to express his preferred event # The lower the order is, the more important is the choice order = models.IntegerField(default=1) def __str__(self): return '{} for {}'.format(str(self.applicant), str(self.event)) class Meta: ordering = ('order', ) unique_together = (('applicant', 'event'), )
class Question(models.Model): """ A generic question type, that can be of several type. If response_type is multichoice you have to specify the answer in the meta field, respecting the following structure: { "choices": { "0": "first option", "1": "second option" } } """ # Formulation of the question question = models.TextField() # Potential additional indications about the questions comment = models.TextField(blank=True) # How to represent the answer response_type = EnumField(AnswerTypes) # If set to true, the applicant will need to fill this field in order to # save his application. always_required = models.BooleanField(default=False) # If set to true, the applicant will need to fill this field in order to # validate his application. finaly_required = models.BooleanField(default=True) # Some extra constraints on the answer meta = JSONField(encoder=DjangoJSONEncoder, default=dict, null=True) def __str__(self): ret = self.question if self.finaly_required: ret += ' (*)' return ret
class Center(AddressableModel): @ChoiceEnum.labels(str.capitalize) class Type(ChoiceEnum): center = 0 restaurant = 1 hotel = 2 pizzeria = 3 other = 4 ugettext_noop("Center") ugettext_noop("Restaurant") ugettext_noop("Hotel") ugettext_noop("Pizzeria") ugettext_noop("Other") name = models.CharField(max_length=64) type = EnumField(Type) is_active = models.BooleanField(default=True) lat = models.DecimalField(default=0, max_digits=16, decimal_places=6) lng = models.DecimalField(default=0, max_digits=16, decimal_places=6) comments = models.TextField(blank=True) objects = CenterQuerySet.as_manager() class Meta: ordering = ('type', 'name', 'city') @property def coordinates(self): return "{:.6f};{:.6f}".format(self.lat, self.lng) @property def has_valid_geolocation(self): return self.lat != 0 and self.lng != 0 def geocode(self, suffix=', FRANCE', geocoder=None): if geocoder is None: geocoder = geopy.geocoders.get_geocoder_for_service('google') location = geocoder().geocode( "{name}, {addr}, {code} {city} {suffix}".format( name=self.name, addr=self.address, code=self.postal_code, city=self.city, suffix=suffix, ), language='fr', timeout=10, ) self.lat = location.latitude self.lng = location.longitude self.save() def normalize(self, suffix=', FRANCE', geocoder=None): if geocoder is None: geocoder = geopy.geocoders.get_geocoder_for_service('google') location = geocoder().geocode( "{addr}, {code} {city} {suffix}".format( addr=self.address, code=self.postal_code, city=self.city, suffix=suffix, ), language='fr', timeout=10, ) addr, city, country = location.address.split(',') if country.strip().lower() != 'france': raise ValueError("Country is not France") code, city = city.split(None, 1) self.address = addr.strip() self.postal_code = code.strip() self.city = city.strip() self.save() def __str__(self): return self.name
class Event(ExportModelOperationsMixin('event'), models.Model): @register_enum(namespace='Event') @ChoiceEnum.labels(lambda e: "Regional event" if e == "semifinal" else e.capitalize()) class Type(ChoiceEnum): qualification = 0 semifinal = 1 final = 2 ugettext_noop("Qualification") ugettext_noop("Regional event") ugettext_noop("Final") edition = models.ForeignKey(Edition, related_name='events', on_delete=models.CASCADE) center = models.ForeignKey(Center, blank=True, null=True, related_name='events', on_delete=models.SET_NULL) type = EnumField(Type, db_index=True) date_begin = models.DateTimeField(blank=True, null=True) date_end = models.DateTimeField(blank=True, null=True) objects = EventManager() class Meta: ordering = ('date_begin', ) @classmethod def semifinals_for_edition(cls, year: int): return cls.objects.select_related('center').filter( edition__year=year, type=cls.Type.semifinal.value) @classmethod def final_for_edition(cls, year: int): return cls.objects.select_related('center').get( edition__year=year, type=cls.Type.final.value) @property def is_finished(self): return self.date_end < timezone.now() @property def is_in_future(self): return timezone.now() < self.date_begin @property def is_active(self): if self.date_begin is None or self.date_end is None: return False return self.date_begin <= timezone.now() <= self.date_end @property def challenge(self): from problems.models import Challenge # circular import try: return Challenge.by_year_and_event_type(self.edition.year, Event.Type(self.type)) except ObjectDoesNotExist: return None @property def short_description(self): return "{} – {}".format( self.center.name, date_format(self.date_begin, "SHORT_DATE_FORMAT")) def __str__(self): return "{edition}: {type} starting {starting}{at}".format( edition=self.edition, type=self.get_type_display(), starting=self.date_begin, at=" at %s" % self.center if self.center else "", )
class ContestantCorrection(ExportModelOperationsMixin('contestant_correction'), models.Model): contestant = models.ForeignKey(Contestant, related_name='corrections', on_delete=models.CASCADE) author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='contestant_correction', null=True, blank=True, on_delete=models.SET_NULL) comment = models.TextField(blank=True) event_type = EnumField(Event.Type, db_index=True) changes = JSONField(blank=True) date_created = models.DateTimeField(default=timezone.now, db_index=True) class Meta: get_latest_by = 'date_created' ordering = ('-' + get_latest_by, ) @property def date_created_utc(self): return int( timezone.make_naive(self.date_created, timezone.utc).timestamp()) def get_changes(self, precision='0.01'): precision = Decimal(precision) event_type = Event.Type(self.event_type) def get_event_description(value): return Event.objects.select_related('center').get( pk=value).short_description def build(): for field in self.contestant.get_score_fields_for_type(event_type): try: value = self.changes[field.name] except KeyError: continue yield { 'field': field, 'type': 'score', 'value': None if value is None else Decimal(value).quantize(precision), } for enum in ('assignation_semifinal', 'assignation_final'): try: value = self.changes[enum] except KeyError: continue yield { 'field': Contestant._meta.get_field(enum), 'type': 'enum', 'value': Assignation.label_for(Assignation(value)), } for nullable, getter in (('assignation_semifinal_event', get_event_description), ): try: value = self.changes[nullable] except KeyError: continue if value is not None: try: value = getter(value) except Exception: continue yield { 'field': Contestant._meta.get_field(nullable), 'type': 'nullable', 'value': value, } return list(build())
class Contestant(ExportModelOperationsMixin('contestant'), models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='contestants', on_delete=models.CASCADE) edition = models.ForeignKey(Edition, related_name='contestants', on_delete=models.CASCADE) school = models.ForeignKey(School, related_name='contestants', null=True, blank=True, default=None, on_delete=models.SET_NULL) shirt_size = EnumField( ShirtSize, null=True, blank=True, db_index=True, verbose_name=_("T-shirt size"), empty_label=_("Choose your size"), help_text=_( "We usually provide unisex Prologin t-shirts to the finalists.")) preferred_language = CodingLanguageField( blank=True, db_index=True, help_text=_( "The programming language you will most likely use during the " "regional events.")) learn_about_contest = EnumField( LearnAboutContest, null=True, blank=True, db_index=True, verbose_name=_("How did you learn about the contest ?"), empty_label=_("Tell us how you discovered the contest")) assignation_semifinal = EnumField( Assignation, default=Assignation.not_assigned.value, verbose_name=_("Regional event assignation status")) assignation_semifinal_wishes = models.ManyToManyField( Event, through='EventWish', related_name='applicants', blank=True, verbose_name=_("Regional event assignation whishes")) assignation_semifinal_event = models.ForeignKey( Event, related_name='assigned_contestants', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_("Regional event assigned event")) assignation_final = EnumField(Assignation, default=Assignation.not_assigned.value, verbose_name=_("Final assignation status")) score_qualif_qcm = models.IntegerField(blank=True, null=True, verbose_name=_("Quiz score")) score_qualif_algo = models.IntegerField( blank=True, null=True, verbose_name=_("Algo exercises score")) score_qualif_bonus = models.IntegerField(blank=True, null=True, verbose_name=_("Bonus score")) score_semifinal_written = models.IntegerField( blank=True, null=True, verbose_name=_("Written exam score")) score_semifinal_interview = models.IntegerField( blank=True, null=True, verbose_name=_("Interview score")) score_semifinal_machine = models.IntegerField( blank=True, null=True, verbose_name=_("Machine exam score")) score_semifinal_bonus = models.IntegerField(blank=True, null=True, verbose_name=_("Bonus score")) score_final = models.IntegerField(blank=True, null=True, verbose_name=_("Score")) score_final_bonus = models.IntegerField(blank=True, null=True, verbose_name=_("Bonus score")) is_home_public = models.BooleanField(default=False) objects = ContestantManager() complete_for_semifinal = ContestantCompleteSemifinalManager() class Meta: unique_together = ('user', 'edition') def get_advancement_enum(self): if self.is_assigned_for_final: return Event.Type.final if self.is_assigned_for_semifinal: return Event.Type.semifinal return Event.Type.qualification def get_score_fields_for_type(self, type: Event.Type) -> dict: mapping = {Event.Type.qualification: 'qualif'} return collections.OrderedDict([ (field, getattr(self, field.name)) for field in self._meta.get_fields() if field.name.startswith('score_{}_'.format( mapping.get(type, type.name))) ]) @cached_property def has_mandatory_info(self): # update ContestantManager.complete_for_{,semi}final accordingly return all( (self.shirt_size is not None, self.preferred_language, self.user.first_name, self.user.last_name, self.user.address, self.user.postal_code, self.user.city, self.user.country, self.user.birthday)) @property def _wish_count(self): try: return self.wish_count except AttributeError: self.wish_count = (self.assignation_semifinal_wishes.filter( type=Event.Type.semifinal.value).distinct().count()) return self.wish_count @property def home_path(self): return os.path.join(settings.HOMES_PATH, str(self.edition.year), '{}.{}'.format(self.user.pk, 'tar.gz')) @property def has_home(self): return os.path.exists(self.home_path) @property def home_filename(self): return 'home-final-{year}-{username}.tar.gz'.format( year=self.edition.year, username=self.user.normalized_username) @property def home_size(self): try: return os.path.getsize(self.home_path) except OSError: return 0 @cached_property def qualification_problems_completion(self): import problems.models challenge = problems.models.Challenge.by_year_and_event_type( self.edition.year, Event.Type.qualification) problem_count = len(challenge.problems) qualif_problem_answers = problems.models.Submission.objects.filter( user=self.user, challenge=challenge.name) completed_problems = qualif_problem_answers.count() return 0 if completed_problems == 0 else 2 if completed_problems == problem_count else 1 @cached_property def qualification_qcm_completion(self): import qcm.models current_qcm = (qcm.models.Qcm.objects.filter( event__edition=self.edition, event__type=Event.Type.qualification.value).first()) if not current_qcm: return 2 question_count = current_qcm.question_count completed_questions = current_qcm.completed_question_count_for(self) return 0 if completed_questions == 0 else 2 if completed_questions == question_count else 1 @cached_property def qualification_completion(self): if not self.is_complete_for_semifinal: return 0 return min(self.qualification_problems_completion, self.qualification_qcm_completion) @cached_property def is_young_enough(self): return self.user.young_enough_to_compete(self.edition.year) @cached_property def has_enough_semifinal_wishes(self): return self._wish_count >= settings.PROLOGIN_SEMIFINAL_MIN_WISH_COUNT @cached_property def is_complete_for_semifinal(self): # update ContestantManager.complete_for_semifinal accordingly return (self.has_mandatory_info and self.is_young_enough and self.has_enough_semifinal_wishes) @property def is_assigned_for_semifinal(self): return self.assignation_semifinal == Assignation.assigned.value @property def has_abandoned_semifinal(self): return self.assignation_semifinal == Assignation.abandoned.value @property def is_ruled_out_for_semifinal(self): return self.assignation_semifinal == Assignation.ruled_out.value @property def is_assigned_for_final(self): return self.assignation_final == Assignation.assigned.value @property def is_ruled_out_for_final(self): return self.assignation_final == Assignation.ruled_out.value @property def completed_qualification(self): return self.is_assigned_for_semifinal or self.is_ruled_out_for_semifinal @property def completed_semifinal(self): return self.is_assigned_for_final or self.is_ruled_out_for_final def score_for(self, event_type: Event.Type): return sum( score or 0 for score in self.get_score_fields_for_type(event_type).values()) @property def score_for_qualification(self): return self.score_for(Event.Type.qualification) @property def score_for_semifinal(self): return self.score_for(Event.Type.semifinal) @property def assignation_final_event(self): return Event.objects.get(edition=self.edition, type=Event.Type.final.value) @property def total_score(self): return sum( getattr(self, field.name) or 0 for field in self._meta.get_fields() if field.name.startswith('score_')) @cached_property def semifinal_challenge(self): from problems.models import Challenge return Challenge.by_year_and_event_type(self.edition.year, Event.Type.semifinal) @cached_property def semifinal_explicitly_unlocked_problems(self): from problems.models import ExplicitProblemUnlock return ExplicitProblemUnlock.objects.filter( challenge=self.semifinal_challenge.name, user=self.user) @cached_property def semifinal_lines_of_code(self): from problems.models import SubmissionCode return sum(1 for code in (SubmissionCode.objects.filter( submission__user=self.user, submission__challenge=self.semifinal_challenge.name).values_list( 'code', flat=True)) for line in code.replace('\r', '\n').split('\n') if line.strip()) @cached_property def semifinal_problems_score(self): from problems.models import Submission return (Submission.objects.filter( user=self.user, challenge=self.semifinal_challenge.name).aggregate( score=Sum(Submission.ScoreFunc))['score']) @cached_property def available_semifinal_problems(self): from problems.models import Challenge, Submission, ExplicitProblemUnlock challenge = self.semifinal_challenge if self.user.is_staff: return list(challenge.problems) elif challenge.type == Challenge.Type.standard: return list(challenge.problems) seed = self.user.pk problem_to_solved_date = {} problem_to_unlock_date = {} difficulty_to_solved = collections.defaultdict(set) for unlock in ExplicitProblemUnlock.objects.filter( user=self.user, challenge=challenge.name): problem = challenge.problem(unlock.problem) problem_to_unlock_date[problem] = unlock.date_created for submission in Submission.objects.filter(user=self.user, challenge=challenge.name, score_base__gt=0): problem = challenge.problem(submission.problem) problem_to_solved_date[problem] = submission.first_code_success( ).date_submitted difficulty_to_solved[problem.difficulty].add(problem) # transform into real dict, so we don't get an empty set but an actual KeyError on missing keys difficulty_to_solved = dict(difficulty_to_solved) dl = challenge.problem_difficulty_list for previous_difficulty, difficulty in itertools.zip_longest( dl, dl[1:]): problems = challenge.problems_of_difficulty(difficulty) try: earliest_unlock_date = min( problem_to_solved_date[problem] for problem in difficulty_to_solved[previous_difficulty]) except KeyError: # no solved problems of previous_difficulty earliest_unlock_date = None if challenge.type == Challenge.Type.all_per_level: if earliest_unlock_date: for problem in problems: problem_to_unlock_date[problem] = earliest_unlock_date elif challenge.type == Challenge.Type.one_per_level: if earliest_unlock_date: with save_random_state(seed): problem_to_unlock_date[random.choice( problems)] = earliest_unlock_date elif challenge.type == Challenge.Type.one_per_level_delayed: # number of solved problems of previous_difficulty quantity = len( difficulty_to_solved.get(previous_difficulty, [])) # unlock (number of solved previous_difficulty) problems of difficulty quantity = min(quantity, len(problems)) if quantity: with save_random_state(seed): for problem in random.sample(problems, quantity): problem_to_unlock_date[ problem] = earliest_unlock_date # unlock a single previous-difficulty problem if still no success at this difficulty if earliest_unlock_date and not difficulty_to_solved.get( difficulty): # previous-difficulty problems that are not already unlocked previous_difficulty_problems = [ problem for problem in challenge.problems_of_difficulty(previous_difficulty) if problem not in problem_to_unlock_date ] if previous_difficulty_problems: if (timezone.now() - earliest_unlock_date ).seconds > challenge.auto_unlock_delay: # waited long enough, unlock a new previous-difficulty problem with save_random_state(seed): problem = random.choice( previous_difficulty_problems) problem_to_unlock_date[ problem] = earliest_unlock_date # should not happen, but you never know (eg. if explicit unlock is removed after it's solved) # assign a fake unlock date to problems that are solved but not unlocked (wtf) for problem, solved_date in problem_to_solved_date.items(): if problem not in problem_to_unlock_date: problem_to_unlock_date[problem] = solved_date return { problem: { 'unlocked': date_unlocked, 'solved': problem_to_solved_date.get(problem) } for problem, date_unlocked in problem_to_unlock_date.items() } def compute_changes(self, new, event_type): changes = { field.name: getattr(new, field.name) for field, value in new.get_score_fields_for_type( event_type).items() if getattr(self, field.name) != value } if event_type == Event.Type.qualification: if self.assignation_semifinal != new.assignation_semifinal: changes['assignation_semifinal'] = new.assignation_semifinal if self.assignation_semifinal_event != new.assignation_semifinal_event: changes['assignation_semifinal_event'] = ( new.assignation_semifinal_event.pk if new.assignation_semifinal_event else None) elif event_type == Event.Type.semifinal: if self.assignation_final != new.assignation_final: changes['assignation_final'] = new.assignation_final return changes def __str__(self): return "{edition}: {user}".format(user=self.user, edition=self.edition)
class ProloginUser(ExportModelOperationsMixin('user'), AbstractUser, AddressableModel): @staticmethod def upload_seed(instance): return 'prologinuser/{}'.format(instance.pk).encode() def upload_avatar_to(self, *args, **kwargs): return upload_path('avatar', using=ProloginUser.upload_seed)(self, *args, **kwargs) def upload_picture_to(self, *args, **kwargs): return upload_path('picture', using=ProloginUser.upload_seed)(self, *args, **kwargs) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] gender = GenderField(blank=True, null=True, db_index=True) school_stage = EnumField(EducationStage, null=True, db_index=True, blank=True, verbose_name=_("Educational stage")) phone = models.CharField(max_length=16, blank=True, verbose_name=_("Phone")) birthday = models.DateField(blank=True, null=True, verbose_name=_("Birth day")) allow_mailing = models.BooleanField( default=True, blank=True, db_index=True, verbose_name=_("Allow Prologin to send me emails"), help_text=_("We only mail you to provide useful information " "during the various stages of the contest. " "We hate spam as much as you do!")) signature = models.TextField(blank=True, verbose_name=_("Signature")) preferred_language = CodingLanguageField( blank=True, db_index=True, verbose_name=_("Preferred coding language")) timezone = TimeZoneField(default=settings.TIME_ZONE, verbose_name=_("Time zone")) preferred_locale = models.CharField(max_length=8, blank=True, verbose_name=_("Locale"), choices=settings.LANGUAGES) avatar = ResizeOnSaveImageField(upload_to=upload_avatar_to, storage=overwrite_storage, fit_into=settings.PROLOGIN_MAX_AVATAR_SIZE, blank=True, verbose_name=_("Profile picture")) picture = ResizeOnSaveImageField( upload_to=upload_picture_to, storage=overwrite_storage, fit_into=settings.PROLOGIN_MAX_AVATAR_SIZE, blank=True, verbose_name=_("Official member picture")) # MD5 password from <2015 Drupal website legacy_md5_password = models.CharField(max_length=32, blank=True) objects = ProloginUserManager() def get_homes(self): return [ c for c in self.contestants.order_by('-edition__year') if c.has_home ] def get_contestants(self): return self.contestants.select_related('edition').order_by( '-edition__year') def get_involved_contestants(self): return self.get_contestants().exclude( assignation_semifinal=Assignation.not_assigned.value) def can_edit_profile(self, edition): if edition is None: # no edition, fallback to allow return True if self.has_perm('users.edit-during-contest'): # privileged return True event, type = edition.phase if event is None: # future or finished, allow return True assigned_semifinal = self.contestants.filter( edition=edition, assignation_semifinal=Assignation.assigned.value).exists() if event == Event.Type.qualification and type == 'corrected' and assigned_semifinal: return False if not assigned_semifinal: return True # below: assigned to semifinal assigned_final = self.contestants.filter( edition=edition, assignation_final=Assignation.assigned.value).exists() if event == Event.Type.semifinal: if type in ('active', 'done'): return False if type == 'corrected' and assigned_final: return False if not assigned_final: return True # below: assigned to final if event == Event.Type.final and type in ('active', 'done'): return False return True @property def preferred_language_enum(self): return Language[self.preferred_language] def plaintext_password(self, event): event_salt = str(event) if event else '' return (base64.urlsafe_b64encode( hashlib.sha1("{}{}{}{}".format( self.first_name, self.last_name, event_salt, settings.PLAINTEXT_PASSWORD_SALT).encode( 'utf-8')).digest()).decode('ascii').translate( settings.PLAINTEXT_PASSWORD_DISAMBIGUATION) [:settings.PLAINTEXT_PASSWORD_LENGTH]) @property def normalized_username(self): return slugify("{}{}".format(self.first_name[:1], self.last_name)) @property def avatar_or_picture(self): if self.avatar: return self.avatar return self.picture @property def picture_or_avatar(self): if self.picture: return self.picture return self.avatar @property def unsubscribe_token(self): user_id = str(self.id).encode() secret = settings.SECRET_KEY.encode() return hashlib.sha256(user_id + secret).hexdigest() def has_partial_address(self): return any((self.address, self.city, self.country, self.postal_code)) def has_complete_address(self): return all((self.address, self.city, self.country, self.postal_code)) def has_complete_profile(self): return self.has_complete_address() and all((self.phone, self.birthday)) def get_absolute_url(self): return reverse('users:profile', args=[self.pk]) def get_unsubscribe_url(self): return '{}{}?uid={}&token={}'.format(settings.SITE_BASE_URL, reverse('users:unsubscribe'), self.id, self.unsubscribe_token) def young_enough_to_compete(self, edition): if not self.birthday: return False last_ok_year = edition - settings.PROLOGIN_MAX_AGE return last_ok_year <= self.birthday.year
class GCCUser(AbstractUser, AddressableModel): @staticmethod def upload_seed(instance): return 'prologinuser/{}'.format(instance.pk).encode() # user have to be imported by the oauth client id = models.IntegerField(primary_key=True) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] gender = GenderField(blank=True, null=True, db_index=True) school_stage = EnumField( EducationStage, null=True, db_index=True, blank=True, verbose_name=_("Educational stage"), ) phone = models.CharField(max_length=16, blank=True, verbose_name=_("Phone")) birthday = models.DateField(blank=True, null=True, verbose_name=_("Birth day")) allow_mailing = models.BooleanField( default=True, blank=True, db_index=True, verbose_name=_("Allow Girls Can Code! to send me emails"), help_text=_( "We only mail you to provide useful information during the " "various stages of the contest. We hate spam as much as " "you do!"), ) signature = models.TextField(blank=True, verbose_name=_("Signature")) timezone = TimeZoneField(default=settings.TIME_ZONE, verbose_name=_("Time zone")) preferred_locale = models.CharField( max_length=8, blank=True, verbose_name=_("Locale"), choices=settings.LANGUAGES, ) @cached_property def participations_count(self): applicants = Applicant.objects.filter(user=self) return sum((applicant.status == ApplicantStatusTypes.confirmed.value) for applicant in applicants) @property def unsubscribe_token(self): user_id = str(self.id).encode() secret = settings.SECRET_KEY.encode() return hashlib.sha256(user_id + secret).hexdigest() def has_partial_address(self): return any((self.address, self.city, self.country, self.postal_code)) def has_complete_address(self): return all((self.address, self.city, self.country, self.postal_code)) def has_complete_profile(self): return self.has_complete_address() and all(( self.first_name, self.last_name, self.email, self.gender, self.birthday, self.phone, )) def get_absolute_url(self): return reverse('users:profile', args=[self.pk]) def get_unsubscribe_url(self): return '{}{}?uid={}&token={}'.format( settings.SITE_BASE_URL, reverse('users:unsubscribe'), self.id, self.unsubscribe_token, )
class Center(AddressableModel): @register_enum(namespace='Center') @ChoiceEnum.labels(str.capitalize) class Type(ChoiceEnum): center = 0 restaurant = 1 hotel = 2 pizzeria = 3 other = 4 ugettext_noop("Center") ugettext_noop("Restaurant") ugettext_noop("Hotel") ugettext_noop("Pizzeria") ugettext_noop("Other") name = models.CharField(max_length=64) type = EnumField(Type) is_active = models.BooleanField(default=True) lat = models.DecimalField(default=0, max_digits=16, decimal_places=6) lng = models.DecimalField(default=0, max_digits=16, decimal_places=6) comments = models.TextField(blank=True) class Meta: ordering = ( 'type', 'name', 'city', ) @property def coordinates(self): return "{:.6f};{:.6f}".format(self.lat, self.lng) @property def has_valid_geolocation(self): return self.lat != 0 and self.lng != 0 def get_osm_url(self): return ('https://www.openstreetmap.org/' f'?mlat={self.lat:.6f}&mlon={self.lng:.6f}' f'#map=16/{self.lat:.6f}/{self.lng:.6f}') def get_absolute_url(self): return reverse("centers:detailMap", kwargs={'id': self.id}) def geocode(self, suffix=', FRANCE', geocoder=None): if geocoder is None: geocoder = geopy.geocoders.get_geocoder_for_service('google') location = geocoder().geocode( "{name}, {addr}, {code} {city} {suffix}".format( name=self.name, addr=self.address, code=self.postal_code, city=self.city, suffix=suffix, ), language='fr', timeout=10) self.lat = location.latitude self.lng = location.longitude self.save() def normalize(self, suffix=', FRANCE', geocoder=None): if geocoder is None: geocoder = geopy.geocoders.get_geocoder_for_service('google') location = geocoder().geocode("{addr}, {code} {city} {suffix}".format( addr=self.address, code=self.postal_code, city=self.city, suffix=suffix, ), language='fr', timeout=10) addr, city, country = location.address.split(',') if country.strip().lower() != 'france': raise ValueError("Country is not France") code, city = city.split(None, 1) self.address = addr.strip() self.postal_code = code.strip() self.city = city.strip() self.save() def __str__(self): return self.name