class StyleAlternateName(models.Model): name = CITextField(unique=True) style = models.ForeignKey( Style, models.CASCADE, related_name='alternate_names') def __str__(self): return self.name
class ManufacturerAlternateName(models.Model): manufacturer = models.ForeignKey( Manufacturer, models.CASCADE, related_name='alternate_names') name = CITextField() def __str__(self): return f'{self.name} for {self.manufacturer_id}'
class PresetItem(Model): """ Preset food item to help user get started with adding food products. """ name = CITextField(max_length=256, unique=True) measure = IntegerField(choices=UnitEnum.choices, null=True) def __str__(self): return f"{self.name} (preset)"
class BeerAlternateName(models.Model): beer = models.ForeignKey(Beer, models.CASCADE, related_name="alternate_names") name = CITextField() def __str__(self): return f"{self.name} for {self.beer_id}"
class Ratable(models.Model): # type: ignore[disallow_any_explicit] # noqa F821 id = models.UUIDField(default=generate_ulid_as_uuid, primary_key=True) description = CITextField() level = models.CharField(max_length=255, blank=True, null=True) inserted_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True
class StyleAlternateName(models.Model): name = CITextField() style = models.ForeignKey(Style, models.CASCADE, related_name="alternate_names") def __str__(self): # pylint: disable=invalid-str-returned return self.name class Meta: constraints = [ models.UniqueConstraint(fields=["name"], name="unique_alt_name_name") ]
class VenueAPIConfiguration(models.Model): venue = models.OneToOneField( Venue, models.CASCADE, related_name="api_configuration", ) url = models.URLField(blank=True, null=True) api_key = models.CharField(max_length=100, blank=True) digital_pour_venue_id = models.CharField(max_length=50, blank=True) digital_pour_location_number = models.PositiveSmallIntegerField( blank=True, null=True, ) untappd_location = models.PositiveIntegerField(blank=True, null=True) untappd_theme = models.PositiveIntegerField(blank=True, null=True) untappd_categories = ArrayField( models.CharField(max_length=50), default=list, blank=True, null=True, ) taphunter_location = models.CharField(max_length=50, blank=True) taphunter_excluded_lists = ArrayField( models.CharField(max_length=50), default=list, blank=True, null=True, ) taplist_io_display_id = models.CharField(max_length=50, blank=True) taplist_io_access_code = models.CharField(max_length=50, blank=True) beermenus_categories = ArrayField( models.TextField(), default=list, blank=True, null=True, ) beermenus_slug = models.CharField(max_length=250, blank=True) arryved_location_id = models.CharField(max_length=50, blank=True) arryved_menu_id = models.CharField(max_length=50, blank=True) arryved_manufacturer_name = CITextField(blank=True) arryved_serving_sizes = ArrayField( models.TextField(), default=list, blank=True, null=True, help_text=_("Short codes for serving sizes of draft pours"), ) arryved_pos_menu_names = ArrayField( models.TextField(), default=list, blank=True, null=True, help_text=_("Individual menus to process from the Arryved POS"), )
class Url(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='url', blank=True) alias = CITextField(max_length=25, blank=True, unique=True) link = models.URLField(max_length=1000) hit = models.IntegerField(default=0) def get_absolute_url(self): return reverse('myapp:home') def __str__(self): return str(self.alias) + " " + str(self.link)
class Style(models.Model): name = CITextField(unique=True) default_color = models.CharField( "HTML color (in hex) to use if the beer has no known color", max_length=9, # #00112233 -> RGBA blank=True, ) def merge_from(self, other_styles): alt_names = [] with transaction.atomic(): for style in other_styles: if style.id == self.id: continue alt_names.append(style.name) style.beers.all().update(style=self) style.alternate_names.all().update(style=self) style.delete() try: # need the second transaction so we can run a query in the # event this fails. Because we're doing a raise in the except # block, the outer transaction will still be aborted in case # of failure. with transaction.atomic(): StyleAlternateName.objects.bulk_create( [ StyleAlternateName( name=name, style=self, ) for name in alt_names ] ) except IntegrityError as exc: existing_names = [ i.name for i in StyleAlternateName.objects.filter( name__in=alt_names, ).exclude( style=self, ) ] raise ValueError( "These alternate names already exist: " f'{", ".join(existing_names)}' ) from exc def __str__(self): # pylint: disable=invalid-str-returned return self.name
class Unit(Model): """ Unit of measurement for each food product. """ class Meta: constraints = [ CheckConstraint(check=Q(convert__gt=0), name="check_unit_convert_positive"), ] symbol = CITextField(max_length=64, unique=True) plural = CITextField(max_length=64, unique=True, null=True, blank=True) code = CITextField(max_length=64, unique=True, null=True, blank=True) measure = IntegerField(choices=UnitEnum.choices) convert = DecimalField( max_digits=MAX_DIGITS, decimal_places=DP_CONVERT, validators=(validate_gt_zero, ), ) def __str__(self): return str(self.symbol) def display(self): return (self.code or self.plural or (self.symbol if self.symbol != "none" else ""))
class PKey(models.Model): """ 이전에 사용한 데이터를 저장하기 위해서 Key-Value 기반의 단순 데이터를 저장한다. TroubleShooting #1. 2018.03.14 테스트 수행 시 database 에 hstore 및 citext 자료형이 없다는 오류가 발생한다. 아래의 SQL 문을 실행시켜야 한다. 그리고 DB 테스트를 수행할 때, default 로 test 데이터베이스를 삭제하므로 -k 옵션을 주어 keep 하도록 해야한다. CREATE EXTENSION hstore WITH SCHEMA public; CREATE EXTENSION citext WITH SCHEMA public; $ python manage.py test -k machina.tests """ key = CITextField(blank=False, unique=True)
class User(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] email = CIEmailField(verbose_name="email", max_length=60, unique=True) username = CITextField(max_length=30, unique=True) first_name = models.CharField(max_length=64, null=True, blank=True) last_name = models.CharField(max_length=64, null=True, blank=True) date_joined = models.DateTimeField(verbose_name="date joined", auto_now_add=True) last_login = models.DateTimeField(verbose_name="last login", auto_now=True) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) objects = UserManager() def __str__(self): return self.email
class Resume(models.Model): # type: ignore[disallow_any_explicit] # noqa F821 class Meta: db_table = "resumes" indexes = [ models.Index(fields=("user", ), name="resumes_user_id_index") ] id = models.UUIDField(default=generate_ulid_as_uuid, primary_key=True) title = CITextField() description = models.TextField(blank=True, null=True) inserted_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) user = models.ForeignKey(User, models.DO_NOTHING, db_constraint=False, db_index=False) def __str__(self): return self.title
class Style(models.Model): name = CITextField(unique=True) def merge_from(self, other_styles): alt_names = [] with transaction.atomic(): for style in other_styles: if style.id == self.id: continue alt_names.append(style.name) style.beers.all().update(style=self) style.alternate_names.all().update(style=self) style.delete() try: # need the second transaction so we can run a query in the # event this fails. Because we're doing a raise in the except # block, the outer transaction will still be aborted in case # of failure. with transaction.atomic(): StyleAlternateName.objects.bulk_create([ StyleAlternateName( name=name, style=self, ) for name in alt_names ]) except IntegrityError: existing_names = [ i.name for i in StyleAlternateName.objects.filter( name__in=alt_names, ).exclude( style=self, ) ] raise ValueError( 'These alternate names already exist: ' f'{", ".join(existing_names)}' ) def __str__(self): return self.name
class ASActivity(ARModel): "All ActivityStreams activities goes here." data = JSONField() domain = CITextField() actor = models.ForeignKey( Account, on_delete=models.DO_NOTHING, related_name='activities', to_field='ap_id') recipients = ArrayField(models.TextField(), null=True) @property def asobject(self): try: return ASObject.objects.get(data__id=self.data["object"]) except KeyError: raise ASObject.DoesNotExist def __str__(self): try: return self.data.get("type", "") + ": " + self.data["id"] except KeyError: return super().__str__() class Meta: verbose_name_plural = 'ASActivities'
class Test(models.Model): """ See, `ref1`: 'https://stackoverfloaw.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers' A New Approach for Verifying URL Uniqueness in Web Crawlers, SPIRE, Pisa, October 19th, 2011 http://homepages.dcc.ufmg.br/~nivio/cursos/ri15/transp/spire11.pdf There are some URL Styles. First, URL has a parameters http:// Second, URL has only path Third, Two URLs have same path, but they use different parameters Category 1. http://test.com/test?param1=a¶m2=b Category 2. http://test.com/test param1=a¶m2=b or, {'param1':'a', 'param2':'b'} Let's simulate the insert process to guess what will happen. Assumption 1. Just distinguish request as a If someone try to insert same request again and again Case Insensitive Text Field CITextField """ """Trouble Shooting, Message " You are trying to add non-nullable field 'something'" 모델을 수정 시 django 에서 추가된 필드에 대해 값을 입력해준다. 따라서, 입력할 값이 지정되지 않았으므로 발생하는 에러이다. (합리적임) default 값을 추가해준다. """ """Tip, Set Create & updated date/time in your models: See, https://www.djangorocks.com/snippets/set-created-updated-datetime-in-your-models.html Samples: <pre><code> class Blog(models.Model): title = models.CharField(max_length=100) added = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) </code></pre> """ # ::: Record Updated Information ::: created = models.DateTimeField(auto_now_add=True) # created at 180316T10:40:10 modfied = models.DateTimeField(auto_now=True) # updated at 180316T10:50:22 # ::: Request Data ::: req_header = HStoreField() # dictionary types method = models.TextField(default='') # ex, PUT, POST, GET, HEAD, OPTIONS, ... # Notes // full_url's max_length option follows RFC 2616 - section 3.2.1 , RFC 7230, See `ref1` full_url = CITextField() # ex. http://test.com/path/for/url?param1¶m2 url = CITextField() # ex. /path/for/url?param1¶m2 url_param = HStoreField(null=True) # param1: value1, param2: value2 body_param = HStoreField(null=True) # body_param1: value1, ... # ::: Client Information ::: # Notes // IPAddressField is deprecated since django 1.7 release client_ip = models.GenericIPAddressField(null=True) # client ip, client_port = models.IntegerField(null=True, blank=True) client_process = CITextField(blank=True, null=True) # like, chrome/12131 {process_name}/{pid} # ::: Server Information ::: hostname = CITextField(blank=True) # www.google.com server_ip = models.GenericIPAddressField(null=True) server_port = models.IntegerField(null=True, blank=True) # ::: Response Data ::: # Field for ResponseHeader res_code = models.IntegerField(blank=True, default=-1, null=True) res_header = HStoreField() # ::: User Interaction ::: # user can update commentary to notify information comment = models.TextField(default="", blank=True) # ::: The section of boolean field ::: is_https = models.BooleanField(default=False) has_body = models.BooleanField(default=False) # ::: Unique Url Checker url_key = models.TextField(null=True) param_hash_key = models.TextField(unique=True, null=True) param_key = models.TextField(unique=True, null=True) class Meta: ordering = ('created', )
class Manufacturer(models.Model): name = CITextField() url = models.URLField(blank=True) location = models.CharField(blank=True, max_length=50) logo_url = models.URLField(blank=True) facebook_url = models.URLField(blank=True) twitter_handle = models.CharField(max_length=50, blank=True) instagram_handle = models.CharField(max_length=50, blank=True) untappd_url = models.URLField(blank=True, null=True) automatic_updates_blocked = models.BooleanField(null=True, default=False) taphunter_url = models.URLField(blank=True, null=True) taplist_io_pk = models.PositiveIntegerField(blank=True, null=True) time_first_seen = models.DateTimeField(blank=True, null=True, default=now) beermenus_slug = models.CharField(max_length=250, blank=True, null=True) class Meta: constraints = [ models.UniqueConstraint(fields=["name"], name="unique_mfg_name"), models.UniqueConstraint(fields=["taplist_io_pk"], name="unique_mfg_taplist_io_pk"), models.UniqueConstraint(fields=["taphunter_url"], name="unique_mfg_taphunter_url"), models.UniqueConstraint(fields=["untappd_url"], name="unique_mfg_untappd_url"), models.UniqueConstraint(fields=["beermenus_slug"], name="unique_mfg_beermenus_slug"), ] def save(self, *args, **kwargs): if not self.beermenus_slug: self.beermenus_slug = None if not self.taphunter_url: self.taphunter_url = None if not self.untappd_url: self.untappd_url = None return super().save(*args, **kwargs) def merge_from(self, other): """Merge the data from other into self""" LOG.info("merging %s into %s", other, self) with transaction.atomic(): other_beers = list(other.beers.all()) my_beers = {i.name.casefold(): i for i in self.beers.all()} for beer in other_beers: beer.manufacturer = self if beer.name.casefold() in my_beers: # we have a duplicate beer. Merge those two first. # merge_from takes care of saving my_beer and deleting # beer # keep the one that was already present my_beer = my_beers[beer.name.casefold()] my_beer.merge_from(beer) else: # good beer.save() ManufacturerAlternateName.objects.filter( manufacturer=other, ).update(manufacturer=self) excluded_fields = { "name", "automatic_updates_blocked", "id", "time_first_seen", } for field in self._meta.fields: field_name = field.name if field_name in excluded_fields: continue other_value = getattr(other, field_name) if getattr(self, field_name) or not other_value: # don't overwrite data that's already there # or isn't set in the other one continue setattr(self, field_name, other_value) self.automatic_updates_blocked = True ManufacturerAlternateName.objects.update_or_create( name=other.name, manufacturer=self, ) other.delete() if other.time_first_seen: if (not self.time_first_seen or self.time_first_seen > other.time_first_seen): self.time_first_seen = other.time_first_seen self.save() def __str__(self): # pylint: disable=invalid-str-returned return self.name
class Beer(models.Model): name = CITextField() style = models.ForeignKey( Style, models.DO_NOTHING, related_name="beers", blank=True, null=True, ) manufacturer = models.ForeignKey( Manufacturer, models.CASCADE, related_name="beers", ) in_production = models.BooleanField(default=True) abv = models.DecimalField( "Alcohol content (% by volume)", max_digits=4, decimal_places=2, blank=True, null=True, ) ibu = models.PositiveSmallIntegerField( "Bitterness (International Bitterness Units)", blank=True, null=True, ) color_srm = models.DecimalField( "Color (Standard Reference Method)", max_digits=4, decimal_places=1, blank=True, null=True, ) untappd_url = models.URLField("Untappd URL (if known)", blank=True, null=True) beer_advocate_url = models.URLField("BeerAdvocate URL (if known)", null=True, blank=True) rate_beer_url = models.URLField("RateBeer URL (if known)", blank=True, null=True) logo_url = models.URLField("Beer logo URL (if known)", blank=True, null=True) color_html = models.CharField( "HTML Color (in hex)", max_length=9, blank=True, # #00112233 -> RGBA ) api_vendor_style = models.CharField( "API vendor-provided style (hidden from API)", max_length=100, blank=True, ) manufacturer_url = models.URLField( "Link to the beer on the manufacturer's website", blank=True, null=True, ) automatic_updates_blocked = models.BooleanField(null=True, default=False) taphunter_url = models.URLField("TapHunter URL (if known)", blank=True, null=True) stem_and_stein_pk = models.PositiveIntegerField(blank=True, null=True) taplist_io_pk = models.PositiveIntegerField(blank=True, null=True) time_first_seen = models.DateTimeField(blank=True, null=True, default=now) tweeted_about = models.BooleanField(default=False) beermenus_slug = models.CharField(max_length=250, blank=True, null=True) class Meta: indexes = [ models.Index(fields=["tweeted_about"]), ] constraints = [ models.CheckConstraint( check=models.Q(abv__gte=0, abv__lte=100) | models.Q(abv__isnull=True), name="abv_positive", ), models.CheckConstraint( check=models.Q(ibu__lte=1000) | models.Q(ibu__isnull=True), name="ibu_not_unreal", ), models.CheckConstraint( check=models.Q(color_srm__lte=500, color_srm__gte=1) | models.Q(color_srm__isnull=True), name="srm_not_unrealistic", ), models.UniqueConstraint( fields=["beermenus_slug"], name="unique_beermenus_slug", ), models.UniqueConstraint( fields=["taplist_io_pk"], name="unique_taplist_io_pk", ), models.UniqueConstraint( fields=["stem_and_stein_pk"], name="unique_stem_and_stein_pk", ), models.UniqueConstraint( fields=["taphunter_url"], name="unique_taphunter_url", ), models.UniqueConstraint( fields=["manufacturer_url"], name="unique_manufacturer_url", ), models.UniqueConstraint( fields=["rate_beer_url"], name="unique_rate_beer_url", ), models.UniqueConstraint( fields=["untappd_url"], name="unique_untappd_url", ), models.UniqueConstraint( fields=["beer_advocate_url"], name="unique_beer_advocate_url", ), models.UniqueConstraint( fields=["name", "manufacturer"], name="unique_beer_per_manufacturer", ), ] def save(self, *args, **kwargs): # force empty IDs to null to avoid running afoul of unique constraints if not self.untappd_url: self.untappd_url = None if not self.beer_advocate_url: self.beer_advocate_url = None if not self.rate_beer_url: self.rate_beer_url = None if not self.manufacturer_url: self.manufacturer_url = None if not self.taphunter_url: self.taphunter_url = None if not self.beermenus_slug: self.beermenus_slug = None return super().save(*args, **kwargs) def __str__(self): # pylint: disable=invalid-str-returned return self.name def render_srm(self): """Convert beer color in SRM into an HTML hex color""" if self.color_html: return self.color_html return render_srm(self.color_srm) def merge_from(self, other): LOG.info("merging %s into %s", other, self) with transaction.atomic(): Tap.objects.filter(beer=other).update(beer=self) BeerAlternateName.objects.filter(beer=other).update(beer=self) try: with transaction.atomic(): BeerPrice.objects.filter(beer=other).update(beer=self) except IntegrityError: LOG.warning("Duplicate prices detected for %s", self) prices_updated = (BeerPrice.objects.filter(beer=other).exclude( venue__in=models.Subquery( BeerPrice.objects.filter( beer=self).values("venue")), ).update(beer=self)) prices_deleted = BeerPrice.objects.filter(beer=other).delete() LOG.info( "Updated %s prices and deleted %s prices", prices_updated, prices_deleted, ) excluded_fields = { "name", "in_production", "automatic_updates_blocked", "manufacturer", "id", "time_first_seen", } for field in self._meta.fields: field_name = field.name if field_name in excluded_fields: continue other_value = getattr(other, field_name) if getattr(self, field_name) or not other_value: # don't overwrite data that's already there # or isn't set in the other one continue setattr(self, field_name, other_value) self.automatic_updates_blocked = True if other.name != self.name: # this will only not happen if manufacturers aren't the same BeerAlternateName.objects.update_or_create( name=other.name, beer=self, ) if other.time_first_seen: if (not self.time_first_seen or self.time_first_seen > other.time_first_seen): self.time_first_seen = other.time_first_seen other.delete() self.save()
class Account(AbstractBaseUser, PermissionsMixin, ARModel): "Customized model for a User (local or remote) or a Subforum" AS_TYPES = ( ('Person', 'Person'), ('Service', 'Bot'), ('Group', 'Subforum'), ) username = CITextField(unique=True, verbose_name='full username') ap_id = models.TextField(unique=True) inbox_uri = models.TextField() outbox_uri = models.TextField() url = models.TextField() is_locked = models.BooleanField(default=False) name = models.TextField(blank=True, default="", null=True) summary = models.TextField(blank=True, default="", null=True) type = models.CharField(choices=AS_TYPES, max_length=50, default="Person") is_superuser = models.BooleanField(default=False) is_staff = models.BooleanField(default=False) email = CITextField(unique=True, null=True, blank=True) following = models.ManyToManyField('self', related_name='followers', symmetrical=False, through='Follow', through_fields=('followee', 'follower')) following_uri = models.TextField() followers_uri = models.TextField() public_key = models.TextField() private_key = models.TextField(null=True) followers_count = models.PositiveIntegerField(null=True) following_count = models.PositiveIntegerField(null=True) posts_count = models.PositiveIntegerField(null=True) USERNAME_FIELD = 'username' EMAIL_FIELD = 'email' REQUIRED_FIELDS = ['email'] @property def preferred_username(self): username, _ = self.username.split("@") return username @property def domain(self): _, domain = self.username.split("@") return domain @property def is_local(self): return self.domain in settings.RBQ_LOCAL_DOMAINS @property def avatar(self): return '' @property def header(self): return '' @property def is_bot(self): return self.type == 'Service' @is_bot.setter def is_bot(self, val): if val: self.type = 'Service' elif self.type == 'Service': self.type = 'Person' objects = AccountManager() subforums = SubforumManager()
class Coupon(models.Model): value = models.IntegerField(_("Value"), help_text=_("Arbitrary coupon value")) code = CITextField( _("Code"), max_length=30, unique=True, blank=True, help_text=_("Leaving this field empty will generate a random code.")) type = models.CharField(_("Type"), max_length=20, choices=COUPON_TYPES) user_limit = models.PositiveIntegerField(_("User limit"), default=1) limit_per_user = models.PositiveIntegerField( _("Coupon redeem limit per User"), default=1 ) created_at = models.DateTimeField(_("Created at"), auto_now_add=True) valid_from = models.DateTimeField( _("Valid from"), default=timezone.now, help_text=_("Defaults to start right away")) valid_until = models.DateTimeField( _("Valid until"), blank=True, null=True, help_text=_("Leave empty for coupons that never expire")) campaign = models.ForeignKey('Campaign', verbose_name=_("Campaign"), blank=True, null=True, related_name='coupons') objects = CouponManager() class Meta: ordering = ['created_at'] verbose_name = _("Coupon") verbose_name_plural = _("Coupons") def __str__(self): return self.code def save(self, *args, **kwargs): if not self.code: self.code = Coupon.generate_code() super(Coupon, self).save(*args, **kwargs) def expired(self): return self.valid_until is not None and self.valid_until < timezone.now() @property def is_redeemed(self): """ Returns true is a coupon is redeemed (completely for all users) otherwise returns false. """ fully_redeemed_users = [ user for user in self.users.select_related('coupon').filter( last_redeemed_at__isnull=False ) if user.fully_redeemed ] return len(fully_redeemed_users) >= self.user_limit and self.user_limit is not 0 @property def last_redeemed_at(self): coupon_user = self.users.filter( last_redeemed_at__isnull=False ).order_by('last_redeemed_at').last() if coupon_user: return coupon_user.last_redeemed_at @classmethod def generate_code(cls, prefix="", segmented=SEGMENTED_CODES): code = "".join(random.choice(CODE_CHARS) for i in range(CODE_LENGTH)) if segmented: code = SEGMENT_SEPARATOR.join([code[i:i + SEGMENT_LENGTH] for i in range(0, len(code), SEGMENT_LENGTH)]) return prefix + code else: return prefix + code def redeem(self, user=None): try: coupon_user = self.users.get(user=user) except CouponUser.DoesNotExist: try: # silently fix unbouned or nulled coupon users coupon_user = self.users.get(user__isnull=True) coupon_user.user = user except CouponUser.DoesNotExist: coupon_user = CouponUser(coupon=self, user=user) coupon_user.last_redeemed_at = timezone.now() coupon_user.redeem_count += 1 coupon_user.save() redeem_done.send(sender=self.__class__, coupon=self)
class Item(Model): """ A food product being tracked. """ class Meta: constraints = [ UniqueConstraint(fields=["user", "name"], name="unique_item_user_name"), UniqueConstraint(fields=["user", "ident"], name="unique_item_user_ident"), CheckConstraint(check=Q(minimum__gte=0), name="check_item_minimum_not_negative"), ] name = CITextField(max_length=256) ident = TextField() user = ForeignKey(settings.AUTH_USER_MODEL, related_name="items", on_delete=CASCADE) unit = ForeignKey("Unit", on_delete=PROTECT, default=1) minimum = DecimalField( max_digits=MAX_DIGITS, decimal_places=DP_QUANTITY, default=0, validators=(MinValueValidator(0), ), ) added = DateTimeField() def save(self, force_insert=False, force_update=False, using=None, update_fields=None): if not self.id: self.added = now() self.ident = slugify(self.name) super().save(force_insert, force_update, using, update_fields) def __str__(self): return str(self.name) @classmethod def with_records(cls, delta=False, asc=False): order = F("added").asc() if asc else F("added").desc() records = Record.objects.order_by(order) if delta: # add quantity delta between this and previous record # use lag or lead depending on order being used here adjacent = Window( expression=(Lag if asc else Lead)("quantity"), partition_by=F("item_id"), order_by=order, ) # find difference, convert to item's unit and cast to correct decimal places expression = Cast( (F("quantity") - Coalesce(adjacent, 0)) / F("item__unit__convert"), DecimalField(max_digits=MAX_DIGITS, decimal_places=DP_QUANTITY), ) records = records.annotate(delta=expression) return (cls.objects.order_by("name").select_related( "unit").prefetch_related(Prefetch("records", records))) @classmethod def with_latest_record(cls): latest = Prefetch( "records", Record.objects.order_by("item_id", "-added").distinct("item_id"), "latest_records", ) return (cls.objects.order_by("name").select_related( "unit").prefetch_related(latest)) def get_absolute_url(self): return reverse("item_get", args=(self.ident, )) @property def latest_record(self): records = getattr(self, "latest_records") return records[0] if records else None def expected_end(self): average = getattr(self, "average") # calculate expected end only if latest record exists with non-zero quantity if self.latest_record is not None and self.latest_record.quantity and average: days = float(self.latest_record.quantity / average) return self.latest_record.added + timedelta(days=days) else: return None
class User(PermissionsMixin, TimeStampedModel, AbstractBaseUser): email = CITextField( verbose_name=_("Email address"), max_length=255, unique=True, ) is_active = models.BooleanField(default=False) is_admin = models.BooleanField(default=False) objects = UserManager() USERNAME_FIELD = "email" REQUIRED_FIELDS = [] name = models.CharField(_("Name"), max_length=100, null=True) email_confirmation_token = models.CharField(max_length=64, default=secrets.token_hex, null=True) email_confirmation_datetime = models.DateTimeField( _("Email confirmation date"), editable=False, null=True) reset_password_token = models.CharField(max_length=64, null=True, editable=False) reset_password_token_expiration_date = models.DateTimeField(null=True, editable=False) class Meta: db_table = "auth_user" def __str__(self): return self.email @property def full_name(self): return self.name @property def is_staff(self): "Is the user a member of staff?" # Simplest possible answer: All admins are staff return self.is_admin def confirm_account(self): self.is_active = True self.email_confirmation_token = None self.email_confirmation_datetime = timezone.now() self.save() def gen_reset_password_token(self): self.reset_password_token = secrets.token_hex() self.reset_password_token_expiration_date = timezone.now() + timedelta( hours=24) self.save() def reset_password(self, password): self.set_password(password) self.reset_password_token = None self.reset_password_token_expiration_date = None self.save()
class Beer(models.Model): name = CITextField() style = models.ForeignKey( Style, models.DO_NOTHING, related_name='beers', blank=True, null=True, ) manufacturer = models.ForeignKey( Manufacturer, models.CASCADE, related_name='beers', ) in_production = models.BooleanField(default=True) abv = models.DecimalField( 'Alcohol content (% by volume)', max_digits=4, decimal_places=2, blank=True, null=True, ) ibu = models.PositiveSmallIntegerField( 'Bitterness (International Bitterness Units)', blank=True, null=True, ) color_srm = models.DecimalField( 'Color (Standard Reference Method)', max_digits=4, decimal_places=1, blank=True, null=True, ) untappd_url = models.URLField(blank=True, null=True, unique=True) beer_advocate_url = models.URLField( 'BeerAdvocate URL (if known)', null=True, blank=True, unique=True, ) rate_beer_url = models.URLField(blank=True, null=True, unique=True) logo_url = models.URLField(blank=True, null=True) color_html = models.CharField( 'HTML Color (in hex)', max_length=9, # #00112233 -> RGBA blank=True, ) api_vendor_style = models.CharField( 'API vendor-provided style (hidden from API)', max_length=100, blank=True, ) manufacturer_url = models.URLField(blank=True, null=True, unique=True) automatic_updates_blocked = models.NullBooleanField(default=False) taphunter_url = models.URLField(blank=True, null=True, unique=True) stem_and_stein_pk = models.PositiveIntegerField( blank=True, null=True, unique=True, ) taplist_io_pk = models.PositiveIntegerField( blank=True, null=True, unique=True, ) time_first_seen = models.DateTimeField(blank=True, null=True, default=now) tweeted_about = models.BooleanField(default=False, db_index=True) beermenus_slug = models.CharField( max_length=250, blank=True, null=True, unique=True, ) def save(self, *args, **kwargs): # force empty IDs to null to avoid running afoul of unique constraints if not self.untappd_url: self.untappd_url = None if not self.beer_advocate_url: self.beer_advocate_url = None if not self.rate_beer_url: self.rate_beer_url = None if not self.manufacturer_url: self.manufacturer_url = None if not self.taphunter_url: self.taphunter_url = None if not self.beermenus_slug: self.beermenus_slug = None return super().save(*args, **kwargs) def __str__(self): return self.name def render_srm(self): if self.color_html: return self.color_html return render_srm(self.color_srm) def merge_from(self, other): LOG.info('merging %s into %s', other, self) with transaction.atomic(): Tap.objects.filter(beer=other).update(beer=self) BeerAlternateName.objects.filter(beer=other).update(beer=self) try: with transaction.atomic(): BeerPrice.objects.filter(beer=other).update(beer=self) except IntegrityError: LOG.warning('Duplicate prices detected for %s', self) prices_updated = BeerPrice.objects.filter(beer=other).exclude( venue__in=models.Subquery( BeerPrice.objects.filter( beer=self).values('venue')), ).update(beer=self) prices_deleted = BeerPrice.objects.filter(beer=other).delete() LOG.info( 'Updated %s prices and deleted %s prices', prices_updated, prices_deleted, ) excluded_fields = { 'name' 'in_production', 'automatic_updates_blocked', 'manufacturer', 'id', 'time_first_seen', } for field in self._meta.fields: field_name = field.name if field_name in excluded_fields: continue other_value = getattr(other, field_name) if getattr(self, field_name) or not other_value: # don't overwrite data that's already there # or isn't set in the other one continue setattr(self, field_name, other_value) self.automatic_updates_blocked = True if other.name != self.name: # this will only not happen if manufacturers aren't the same BeerAlternateName.objects.update_or_create( name=other.name, beer=self, ) if other.time_first_seen: if not self.time_first_seen or \ self.time_first_seen > other.time_first_seen: self.time_first_seen = other.time_first_seen other.delete() self.save() class Meta: unique_together = [ ('name', 'manufacturer'), ]
class Manufacturer(models.Model): name = CITextField(unique=True) url = models.URLField(blank=True) location = models.CharField(blank=True, max_length=50) logo_url = models.URLField(blank=True) facebook_url = models.URLField(blank=True) twitter_handle = models.CharField(max_length=50, blank=True) instagram_handle = models.CharField(max_length=50, blank=True) untappd_url = models.URLField(blank=True, unique=True, null=True) automatic_updates_blocked = models.NullBooleanField(default=False) taphunter_url = models.URLField(blank=True, null=True, unique=True) taplist_io_pk = models.PositiveIntegerField( blank=True, null=True, unique=True, ) time_first_seen = models.DateTimeField(blank=True, null=True, default=now) def merge_from(self, other): LOG.info('merging %s into %s', other, self) with transaction.atomic(): other_beers = list(other.beers.all()) my_beers = {i.name.casefold(): i for i in self.beers.all()} for beer in other_beers: beer.manufacturer = self if beer.name.casefold() in my_beers: # we have a duplicate beer. Merge those two first. # merge_from takes care of saving my_beer and deleting # beer # keep the one that was already present my_beer = my_beers[beer.name.casefold()] my_beer.merge_from(beer) else: # good beer.save() for alternate_name in other.alternate_names.all(): alternate_name.beer = self alternate_name.save() excluded_fields = { 'name', 'automatic_updates_blocked', 'id', 'time_first_seen', } for field in self._meta.fields: field_name = field.name if field_name in excluded_fields: continue other_value = getattr(other, field_name) if getattr(self, field_name) or not other_value: # don't overwrite data that's already there # or isn't set in the other one continue setattr(self, field_name, other_value) self.automatic_updates_blocked = True ManufacturerAlternateName.objects.update_or_create( name=other.name, manufacturer=self, ) other.delete() if other.time_first_seen: if not self.time_first_seen or \ self.time_first_seen > other.time_first_seen: self.time_first_seen = other.time_first_seen self.save() def __str__(self): return self.name
class OrgUnit(models.Model): VALIDATION_NEW = "NEW" VALIDATION_VALID = "VALID" VALIDATION_REJECTED = "REJECTED" VALIDATION_STATUS_CHOICES = ( (VALIDATION_NEW, _("new")), (VALIDATION_VALID, _("valid")), (VALIDATION_REJECTED, _("rejected")), ) name = models.CharField(max_length=255) uuid = models.TextField(null=True, blank=True, db_index=True) custom = models.BooleanField(default=False) validated = models.BooleanField( default=True, db_index=True) # TO DO : remove in a later migration validation_status = models.CharField(max_length=25, choices=VALIDATION_STATUS_CHOICES, default=VALIDATION_NEW) version = models.ForeignKey("SourceVersion", null=True, blank=True, on_delete=models.CASCADE) parent = models.ForeignKey("OrgUnit", on_delete=models.CASCADE, null=True, blank=True) path = PathField(null=True, blank=True, unique=True) aliases = ArrayField(CITextField(max_length=255, blank=True), size=100, null=True, blank=True) org_unit_type = models.ForeignKey(OrgUnitType, on_delete=models.CASCADE, null=True, blank=True) sub_source = models.TextField( null=True, blank=True) # sometimes, in a given source, there are sub sources source_ref = models.TextField(null=True, blank=True, db_index=True) geom = MultiPolygonField(null=True, blank=True, srid=4326, geography=True) simplified_geom = MultiPolygonField(null=True, blank=True, srid=4326, geography=True) catchment = MultiPolygonField(null=True, blank=True, srid=4326, geography=True) geom_ref = models.IntegerField(null=True, blank=True) gps_source = models.TextField(null=True, blank=True) location = PointField(null=True, blank=True, geography=True, dim=3, srid=4326) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) creator = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) objects = OrgUnitManager.from_queryset(OrgUnitQuerySet)() class Meta: indexes = [GistIndex(fields=["path"], buffering=True)] def save(self, *args, skip_calculate_path: bool = False, **kwargs): """Override default save() to make sure that the path property is calculated and saved, for this org unit and its children. :param skip_calculate_path: use with caution - can be useful in scripts where the extra transactions would be a burden, but the path needs to be set afterwards """ if skip_calculate_path: super().save(*args, **kwargs) else: with transaction.atomic(): super().save(*args, **kwargs) OrgUnit.objects.bulk_update(self.calculate_paths(), ["path"]) def calculate_paths(self, force_recalculate: bool = False ) -> typing.List["OrgUnit"]: """Calculate the path for this org unit and all its children. This method will check if this org unit path should change. If it is the case (or if force_recalculate is True), it will update the path property for the org unit and its children, and return all the modified records. Please note that this method does not save the modified records. Instead, they are updated in bulk in the save() method. :param force_recalculate: calculate path for all descendants, even if this org unit path does not change """ # For now, we will skip org units that have a parent without a path. # The idea is that a management command (set_org_unit_path) will handle the initial seeding of the # path field, starting at the top of the pyramid. Once this script has been run and the field is filled for # all org units, this should not happen anymore. # TODO: remove condition below if self.parent is not None and self.parent.path is None: return [] # keep track of updated records updated_records = [] base_path = [] if self.parent is None else list(self.parent.path) new_path = [*base_path, str(self.pk)] path_has_changed = new_path != self.path if path_has_changed: self.path = new_path updated_records += [self] if path_has_changed or force_recalculate: for child in self.orgunit_set.all(): updated_records += child.calculate_paths(force_recalculate) return updated_records def __str__(self): return "%s %s %d" % (self.org_unit_type, self.name, self.id if self.id else -1) def as_dict_for_mobile_lite(self): return { "n": self.name, "id": self.id, "p": self.parent_id, "out": self.org_unit_type_id, "c_a": self.created_at.timestamp() if self.created_at else None, "lat": self.location.y if self.location else None, "lon": self.location.x if self.location else None, "alt": self.location.z if self.location else None, } def as_dict_for_mobile(self): return { "name": self.name, "id": self.id, "parent_id": self.parent_id, "org_unit_type_id": self.org_unit_type_id, "org_unit_type_name": self.org_unit_type.name if self.org_unit_type else None, "validation_status": self.validation_status if self.org_unit_type else None, "created_at": self.created_at.timestamp() if self.created_at else None, "updated_at": self.updated_at.timestamp() if self.updated_at else None, "latitude": self.location.y if self.location else None, "longitude": self.location.x if self.location else None, "altitude": self.location.z if self.location else None, } def as_dict(self, with_groups=True): res = { "name": self.name, "short_name": self.name, "id": self.id, "source": self.version.data_source.name if self.version else None, "source_ref": self.source_ref, "parent_id": self.parent_id, "org_unit_type_id": self.org_unit_type_id, "org_unit_type_name": self.org_unit_type.name if self.org_unit_type else None, "created_at": self.created_at.timestamp() if self.created_at else None, "updated_at": self.updated_at.timestamp() if self.updated_at else None, "aliases": self.aliases, "validation_status": self.validation_status, "latitude": self.location.y if self.location else None, "longitude": self.location.x if self.location else None, "altitude": self.location.z if self.location else None, "has_geo_json": True if self.simplified_geom else False, "version": self.version.number if self.version else None, } if hasattr(self, "search_index"): res["search_index"] = self.search_index return res def as_dict_with_parents(self, light=False, light_parents=True): res = { "name": self.name, "short_name": self.name, "id": self.id, "sub_source": self.sub_source, "sub_source_id": self.sub_source, "source_ref": self.source_ref, "source_url": self.version.data_source.credentials.url if self.version and self.version.data_source and self.version.data_source.credentials else None, "parent_id": self.parent_id, "validation_status": self.validation_status, "parent_name": self.parent.name if self.parent else None, "parent": self.parent.as_dict_with_parents(light=light_parents, light_parents=light_parents) if self.parent else None, "org_unit_type_id": self.org_unit_type_id, "created_at": self.created_at.timestamp() if self.created_at else None, "updated_at": self.updated_at.timestamp() if self.updated_at else None, "aliases": self.aliases, "latitude": self.location.y if self.location else None, "longitude": self.location.x if self.location else None, "altitude": self.location.z if self.location else None, "has_geo_json": True if self.simplified_geom else False, } if not light: # avoiding joins here res["groups"] = [ group.as_dict(with_counts=False) for group in self.groups.all() ] res["org_unit_type_name"] = self.org_unit_type.name if self.org_unit_type else None res["org_unit_type"] = self.org_unit_type.as_dict( ) if self.org_unit_type else None res["source"] = self.version.data_source.name if self.version else None res["source_id"] = self.version.data_source.id if self.version else None res["version"] = self.version.number if self.version else None if hasattr(self, "search_index"): res["search_index"] = self.search_index return res def as_small_dict(self): res = { "name": self.name, "id": self.id, "parent_id": self.parent_id, "validation_status": self.validation_status, "parent_name": self.parent.name if self.parent else None, "source": self.version.data_source.name if self.version else None, "source_ref": self.source_ref, "parent": self.parent.as_small_dict() if self.parent else None, "org_unit_type_name": self.org_unit_type.name if self.org_unit_type else None, } if hasattr(self, "search_index"): res["search_index"] = self.search_index return res def as_dict_for_csv(self): return { "name": self.name, "id": self.id, "source_ref": self.source_ref, "parent_id": self.parent_id, "org_unit_type": self.org_unit_type.name, } def as_location(self): res = { "id": self.id, "name": self.name, "short_name": self.name, "latitude": self.location.y if self.location else None, "longitude": self.location.x if self.location else None, "altitude": self.location.z if self.location else None, "has_geo_json": True if self.simplified_geom else False, "org_unit_type": self.org_unit_type.name if self.org_unit_type else None, "org_unit_type_depth": self.org_unit_type.depth if self.org_unit_type else None, "source_id": self.version.data_source.id if self.version else None, "source_name": self.version.data_source.name if self.version else None, } if hasattr(self, "search_index"): res["search_index"] = self.search_index return res def source_path(self): """DHIS2-friendly path built using source refs""" path_components = [] cur = self while cur: if cur.source_ref: path_components.insert(0, cur.source_ref) cur = cur.parent if len(path_components) > 0: return "/" + ("/".join(path_components)) return None