class TemplateContent(BaseContentMixin): """ Model for template related content BaseContentMixin is Polymorphic, allowing you to get all contents within few SQL queries (one per child models) Attributes: name (CharField): If you need to name your template for UX sugar path (CharField): Your template path, as used in "render" or include/extend """ template = models.ForeignKey( "stack_it.Template", verbose_name=_("Template"), related_name="contents", on_delete=models.CASCADE, ) objects = PolymorphicManager() class Meta: verbose_name = _("Template") verbose_name_plural = _("Template") unique_together = (("template", "key"),) def __str__(self): return self.key
class BaseProduct(PolymorphicModel): name = models.CharField(verbose_name='Name/model', max_length=30) price = models.FloatField(verbose_name='Price') quantity = models.IntegerField(verbose_name='Quantity', validators=[MinValueValidator(0)]) recommended = models.BooleanField( default=False) # recommended products are show on home page additional_spec = models.CharField( verbose_name='Additional specifications', max_length=200, blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) objects = PolymorphicManager() class Meta: db_table = 'products' verbose_name = 'Base Product' verbose_name_plural = 'Base Products' def __str__(self): return self.name
class Message(PolymorphicModel): sender = models.ForeignKey(get_user_model(), verbose_name=_('sender'), related_name='sent_messages', on_delete=models.PROTECT, help_text='The user who authored the message') parent = models.ForeignKey( 'self', verbose_name=_('parent'), related_name='replies', on_delete=models.CASCADE, null=True, blank=True, help_text='The main message in a message thread') content = models.TextField(_('content')) # Default fields. Used for record-keeping. uuid = models.UUIDField(default=uuid.uuid4, editable=False) slug = models.SlugField(_('slug'), max_length=250, unique=True, editable=False, blank=True) created_at = models.DateTimeField(_('created at'), auto_now_add=True, editable=False) updated_at = models.DateTimeField(_('uploaded at'), auto_now=True, editable=False) objects = PolymorphicManager() custom_objects = MessageManager() class Meta: db_table = 'vestlus_messages' indexes = [models.Index(fields=['created_at', 'sender', 'parent'])] ordering = ['-created_at', 'sender'] @property def preview(self): return f'{truncatewords(self.content, 20)}...' @property def avatar(self): try: return self.sender.photo.url except AttributeError: return 'https://github.com/octocat.png' def save(self, *args, **kwargs): self.slug = slugify(f'{str(self.uuid)[-12:]}') super().save(*args, **kwargs) def __str__(self): return f'{self.slug}' def get_absolute_url(self): return reverse('vestlus:message-detail', kwargs={'slug': self.slug})
class Base(AdminLinkMixin, PolymorphicModel): slug = models.SlugField() objects = PolymorphicManager() if django.VERSION < (2, 0): class Meta: manager_inheritance_from_future = True def __str__(self): return "Base %s" % self.slug
class PageContent(BaseContentMixin): """ Model for page related content BaseContentMixin is Polymorphic, allowing you to get all contents within few SQL queries (one per child models) Attributes: page (ForeignKey): Page instance where content belongs """ page = models.ForeignKey( "stack_it.Page", verbose_name=_("Page"), related_name="contents", on_delete=models.CASCADE, ) objects = PolymorphicManager() class Meta: verbose_name = _("Page Content") verbose_name_plural = _("Page Contents") unique_together = (("page", "key"),)
class MROBase3(models.Model): objects = PolymorphicManager()
class ModelWithMyManagerDefault(ShowFieldTypeAndContent, Model2A): my_objects = MyManager() objects = PolymorphicManager() field4 = models.CharField(max_length=10)
class Hook(PolymorphicModel, BaseConcurrentModel): name = models.CharField(primary_key=True, max_length=255) objects = PolymorphicManager()
class ProductCard(PolymorphicModel, OfferPage, Image, Weighable, Dimensional): class Meta: # abstract = True pass # Custom managers objects = PolymorphicManager() public = PublicProductCardManager() # KEY-CLASSES attributevalues_relation_class = None attributevalue_class = None product_model_class = None # KEYS AND M2M attribute_values = None product_model = None # Image upload_image_to = None image_key_attribute = 'get_image_name' # Search update_search = False vendor_code = models.CharField(verbose_name="Артикул", max_length=1024, db_index=True, unique=True, validators=[ RegexValidator( regex=r'^[-_a-z\d]+$', message='vendor_code valid error', ) ]) product_type = models.CharField( verbose_name="Тип продукта", max_length=128, choices=(("", "Не задано"), ("BACKPACK", "BACKPACK"), ("BAG", "BAG"), ("SUITCASE", "SUITCASE"), ("PURSE", "PURSE")), default="", blank=True, ) @property def data_for_delivery(self): result = { "product_type": self.product_type, "price": self.price, "purchase_price": self.purchase_price, "vendor": self.vendor } if self.weigh: result['weigh'] = math.ceil(self.weigh / 1000) if self.dimensions: result['dimensions'] = self.dimensions return result product_aditional_image_class = None @property def get_image_name(self): return self.model.replace('/', '-') model = models.CharField(verbose_name="Модель", max_length=1024, db_index=True) def get_url(self): return self.slug @disallowed_before_creation def add_attribute_value(self, attribute_value_instance): if attribute_value_instance.pk: try: self.attributevalues_relation_class.objects.create( product=self, attribute_value=attribute_value_instance) except IntegrityError: pass else: raise DisallowedBeforeCreationException( 'attributes_value_instance must be creatied') @disallowed_before_creation def del_attribute_value(self, attribute_value_instance): if attribute_value_instance.pk: try: self.attributevalues_relation_class.objects.get( product=self, attribute_value=attribute_value_instance).delete() except ObjectDoesNotExist: pass else: raise DisallowedBeforeCreationException( 'attribute_value_instance must be creatied') @disallowed_before_creation def clear_attribute_value(self): self.attributevalues_relation_class.objects.filter( product=self).delete() @property @disallowed_before_creation def attributes(self): result = OrderedDict() for av in self.attribute_values.select_related().order_by( '-attribute__order', '-order'): result[av.attribute] = result.get(av.attribute, []) + [av] return result @property @disallowed_before_creation def additional_images(self): return self.product_aditional_image_class.objects.filter( product=self).order_by('order', 'id') @property def public_id(self): return self.get_public_id() @property def has_photo(self): return self.image != self.image.field.default @property @disallowed_before_creation def photo_checksums(self): checksums = set() images = self.product_aditional_image_class.objects.filter( product=self, ) if self.has_photo: checksums.add(md5_file_checksum(self.image.path)) for image in images: checksums.add(md5_file_checksum(image.image.path)) return checksums def generate_image_from_main(self): max_order = self.product_aditional_image_class.objects.filter( product=self, ).aggregate(Max('order'))['order__max'] if max_order is None: max_order = 0 else: max_order += 1 instance = self.product_aditional_image_class( product=self, image=self.image, order=max_order, ) instance.save() def replace_main_image_by_file(self, image_file): self.generate_image_from_main() self.image = image_file update_search = not settings.DEBUG self.save(update_search=update_search) def replace_main_image_by_additional(self, image_id): try: instance = self.product_aditional_image_class.objects.get( id=image_id) self.generate_image_from_main() self.image = instance.image update_search = not settings.DEBUG self.save(update_search=update_search) instance.delete() except self.product_aditional_image_class.DoesNotExist: pass def add_photo(self, filename): fp = open(filename, 'rb') img_file = File(fp) if self.has_photo: instance = self.product_aditional_image_class( product=self, image=img_file, ) instance.save() else: self.image = img_file self.save() fp.close() def get_public_id(self): msg = "Method get_public_id() must be implemented by subclass: `{}`" raise NotImplementedError(msg.format(self.__class__.__name__)) def get_scoring(self): scoring = 0 if self.has_photo: if self.scoring != 0: scoring = self.scoring else: scoring = random.randint(100, 150) return scoring def get_image_url(self): return self.image.url def get_modifications(self): if self.product_model is not None: return self.product_model.products.exclude(id=self.id) return [] @property def modifications(self): return self.get_modifications() def __str__(self): return 'Product card: {0}'.format(self.slug) def save(self, update_search=False, *args, **kwargs): self.update_search = update_search self.scoring = self.get_scoring() super(ProductCard, self).save(*args, **kwargs)
class BaseResource(PolymorphicModel, BaseConcurrentModel): # name = models.CharField(u'资源名', max_length=64, blank=False, null=False) departments = models.ManyToManyField(Department, blank=True) objects = PolymorphicManager()
class MROBase3(models.Model): base_3_id = models.AutoField( primary_key=True ) # make sure 'id' field doesn't clash, detected by Django 1.11 objects = PolymorphicManager()
class Source(PolymorphicModel): """ A project source. """ project = models.ForeignKey( Project, null=True, blank=True, on_delete=NON_POLYMORPHIC_CASCADE, related_name="sources", ) address = models.TextField( null=False, blank=False, help_text="The address of the source. e.g. github://org/repo/subpath", ) path = models.TextField( null=True, blank=True, help_text= "The file or folder name, or path, that the source is mapped to.", ) order = models.PositiveIntegerField( null=True, blank=True, help_text="The order that the source should be pulled into the project " "(allows explicit overwriting of one source by another).", ) creator = models.ForeignKey( User, null=True, # Should only be null if the creator is deleted blank=True, on_delete=models.SET_NULL, related_name="sources_created", help_text="The user who created the source.", ) created = models.DateTimeField( auto_now_add=True, help_text="The time the source was created.") updated = models.DateTimeField( auto_now=True, help_text="The time the source was last changed.") subscription = models.JSONField( null=True, blank=True, help_text= "Information from the source provider (e.g. Github, Google) when a watch subscription was created.", ) jobs = models.ManyToManyField( Job, help_text= "Jobs associated with this source. e.g. pull, push or convert jobs.", ) # The default object manager which will fetch data from multiple # tables (one query for each type of source) objects = PolymorphicManager() # An additional manager which will only fetch data from the `Source` # table. objects_base = models.Manager() class Meta: constraints = [ # Constraint to ensure that sources are not duplicated within a # project. Note that sources can share the same `path` e.g. # two sources both targetting a `data` folder. models.UniqueConstraint(fields=["project", "address"], name="%(class)s_unique_project_address") ] def __str__(self) -> str: return self.address @property def type_class(self) -> str: """ Get the name the class of a source instance. If this is an instance of a derived class then returns that class name. Otherwise, fetches the name of the model class based on the `polymorphic_ctype_id` (with caching) and then cases it properly. """ if self.__class__.__name__ != "Source": return self.__class__.__name__ all_lower = ContentType.objects.get_for_id( self.polymorphic_ctype_id).model for source_type in SourceTypes: if source_type.name.lower() == all_lower: return source_type.name return "" @property def type_name(self) -> str: """ Get the name of the type of a source instance. The `type_name` is intended for use in user interfaces. This base implementation simply drops `Source` from the end of the name. Can be overridden in derived classes if necessary. """ return self.type_class[:-6] @staticmethod def class_from_type_name(type_name: str) -> Type["Source"]: """Find the class matching the type name.""" for source_type in SourceTypes: if source_type.name.lower().startswith(type_name.lower()): return source_type.value raise ValueError( 'Unable to find class matching "{}"'.format(type_name)) def make_address(self) -> str: """ Create a human readable string representation of the source instance. These are intended to be URL-like human-readable and -writable shorthand representations of sources (e.g. `github://org/repo/sub/path`; `gdoc://378yfh2yg362...`). They are used in admin lists and in API endpoints to allowing quick specification of a source (e.g. for filtering). Should be parsable by the `parse_address` method of each subclass. """ return "{}://{}".format(self.type_name.lower(), self.id) @staticmethod def coerce_address(address: Union[SourceAddress, str]) -> SourceAddress: """ Coerce a string into a `SourceAddress`. If the `address` is already an instance of `SourceAddress` it will be returned unchanged. Otherwise, the `parse_address` method of each source type (ie. subclass of `Source`) will be called. The first class that returns a `SourceAddress` wins. """ if isinstance(address, SourceAddress): return address for source_type in SourceTypes: result = source_type.value.parse_address(address) if result is not None: return result raise ValueError('Unable to parse source address "{}"'.format(address)) @classmethod def parse_address(cls, address: str, naked: bool = False, strict: bool = False) -> Optional[SourceAddress]: """ Parse a string into a `SourceAddress`. This default implementation just matches the default string representation of a source, as generated by `Source.__str__`. Derived classes should override this method to match their own `__str__` method. """ type_name = cls.__name__[:-6] if address.startswith(type_name.lower() + "://"): return SourceAddress(type_name) if strict: raise ValidationError( "Invalid source identifier: {}".format(address)) return None @staticmethod def query_from_address( address_or_string: Union[SourceAddress, str], prefix: Optional[str] = None, ) -> models.Q: """ Create a query object for a source address. Given a source address, constructs a Django `Q` object which can be used in ORM queries. Use the `prefix` argument when you want to use this function for a related field. For example, Source.query_from_address({ "type": "Github", "repo": "org/repo", "subpath": "folder" }, prefix="sources") is equivalent to : Q( sources__githubsource__repo="org/repo", sources__githubsource__subpath="folder" ) """ address = Source.coerce_address(address_or_string) front = ("{}__".format(prefix) if prefix else "") + address.type.__name__.lower() kwargs = dict([("{}__{}".format(front, key), value) for key, value in address.items()]) return models.Q(**kwargs) @staticmethod def from_address(address_or_string: Union[SourceAddress, str], **other) -> "Source": """ Create a source instance from a source address. Given a source address, creates a new source instance of the specified `type` with fields set from the address. The `other` parameter can be used to set additional fields on the created source. """ address = Source.coerce_address(address_or_string) return address.type.objects.create(**address, **other) def to_address(self) -> SourceAddress: """ Create a source address from a source instance. The inverse of `Source.from_address`. Used primarily to create a pull job for a source. """ all_fields = [field.name for field in self._meta.fields] class_fields = [ name for name in self.__class__.__dict__.keys() if name in all_fields ] return SourceAddress( self.type_name, **dict([(name, value) for name, value in self.__dict__.items() if name in class_fields]), ) def get_secrets(self, user: User) -> Dict: """ Get any secrets required to pull or push the source. This method should be overridden by derived classes to return a secrets (e.g. OAuth tokens, API keys) specific to the source (if necessary). """ return {} def get_url(self, path: Optional[str] = None): """ Create a URL for users to visit the source on the external site. path: An optional path to a file within the source (for multi-file sources such as GitHub repos). """ raise NotImplementedError def get_event_url(self): """ Create a URL for source events to be sent to. This is the Webhook URL used by Github, Google and other source providers to send event data to when a source has been `watch()`ed. """ return ("https://" + settings.PRIMARY_DOMAIN + reverse( "api-projects-sources-event", kwargs=dict(project=self.project.id, source=self.id), )) def pull(self, user: Optional[User] = None) -> Job: """ Pull the source to the filesystem. Creates a job, and adds it to the source's `jobs` list. """ source = self.to_address() source["type"] = source.type_name description = "Pull {0}" if self.type_class == "UploadSource": description = "Collect {0}" description = description.format(self.address) job = Job.objects.create( project=self.project, creator=user or self.creator, method=JobMethod.pull.value, params=dict(source=source, path=self.path), description=description, secrets=self.get_secrets(user), **Job.create_callback(self, "pull_callback"), ) self.jobs.add(job) return job @transaction.atomic def pull_callback(self, job: Job): """ Update the files associated with this source. """ result = job.result if not result: return from projects.models.files import File, get_modified # All existing files for the source are made "non-current" (i.e. will not # be displayed in project working directory but are retained for history) File.objects.filter(project=self.project, source=self).update(current=False, updated=timezone.now()) # Do a batch insert of files. This is much faster when there are a lot of file # than inserting each file individually. File.objects.bulk_create([ File( project=self.project, path=path, job=job, source=self, updated=timezone.now(), modified=get_modified(info), size=info.get("size"), mimetype=info.get("mimetype"), encoding=info.get("encoding"), fingerprint=info.get("fingerprint"), ) for path, info in result.items() ]) # Asynchronously check whether the project's image needs to be updated given # that there are updated files. update_image_for_project.delay(project_id=self.project.id) def extract(self, review, user: Optional[User] = None, filters: Optional[Dict] = None) -> Job: """ Extract a review from a project source. Creates a job, and adds it to the source's `jobs` list. Note: the jobs callback is `Review.extract_callback`. """ source = self.to_address() source["type"] = source.type_name description = "Extract review from {0}" description = description.format(self.address) job = Job.objects.create( project=self.project, creator=user or self.creator, method=JobMethod.extract.value, params=dict(source=source, filters=filters), description=description, secrets=self.get_secrets(user), **Job.create_callback(review, "extract_callback"), ) self.jobs.add(job) return job def push(self) -> Job: """ Push from the filesystem to the source. Creates a `Job` having the `push` method and a dictionary of `source` attributes sufficient to push it. This may include authentication tokens. """ raise NotImplementedError( "Push is not implemented for class {}".format( self.__class__.__name__)) def watch(self, user: User): """ Create a subscription to listen to events for the source. """ raise NotImplementedError( "Watch is not implemented for class {}".format( self.__class__.__name__)) def unwatch(self, user: User): """ Remove a subscription to listen to events for the source. """ raise NotImplementedError( "Unwatch is not implemented for class {}".format( self.__class__.__name__)) def event(self, data: dict, headers: dict = {}): """ Handle an event notification. Passes on the event to the project's event handler with this source added to the context. """ self.project.event(data=data, source=self) def preview(self, user: User) -> Job: """ Generate a HTML preview of a source. Creates a `series` job comprising a `pull` job followed by a `convert` job. """ preview = Job.objects.create(project=self.project, creator=user, method="series") preview.children.add(self.pull(user)) preview.children.add(self.convert(user, to="html")) return preview # Properties related to jobs. Provide shortcuts for # obtaining info such as the files created in the last pull, # or the time of the last push @property def is_active(self) -> bool: """ Is the source currently active. A source is considered active if it has an active job. The `since` parameter is included to ignore old, orphaned jobs which may have not have had their status updated. """ since = datetime.timedelta(minutes=15) return (self.jobs.filter( is_active=True, created__gte=timezone.now() - since).count() > 0) def get_downstreams(self, current=True): """ Get the downstream files. The equivalent of `File.get_downstreams()`. """ return self.files.filter(current=current) def get_jobs(self, n=10) -> List[Job]: """ Get the jobs associated with this source. """ return self.jobs.order_by("-created").select_related("creator")[:n] def save(self, *args, **kwargs): """ Save the source. An override to ensure necessary fields are set. """ if not self.address: self.address = self.make_address() return super().save(*args, **kwargs) def delete(self, *args, **kwargs): """ Delete the source. Override to unwatch a source before deleting it. This avoids the source provider continuing to send events for the source when it no longer exists. """ if self.subscription: # Because we do not have the quest user here, use # the source creator for the unwatch and log any # exceptions as warnings only. try: self.unwatch(self.creator) except Exception as exc: logger.warning(str(exc), exc_info=True) return super().delete(*args, **kwargs)